/* ==========================================================================
   The Wear Bear — styles
   Mobile-first. Big type, big tap targets, one screen, no menus.
   Sky colours are driven by body[data-sky="…"] so the whole page reacts to
   the weather without any inline styles.
   ========================================================================== */

/*
 * Baloo 2 is loaded in fonts.css and self-hosted. The system rounded stack
 * behind it is a real fallback, not decoration: it renders acceptably on the
 * first paint before the face arrives, and offline if the cache ever misses.
 */
:root {
  --font: 'Baloo 2', ui-rounded, "SF Pro Rounded", Nunito, system-ui,
          -apple-system, "Segoe UI", Roboto, sans-serif;

  --ink: #4a3324;
  --ink-soft: #7a6149;
  /* Text that sits directly on the sky rather than on a card. Identical to
     --ink by day; inverted at night, where dark-on-dark would vanish. */
  /* Ink for the bear's line, which sits on the sky. */
  --on-sky: #4a3324;
  /* Ink for the readout, which sits on the page. Separate tokens because the
     restructure put them on different surfaces — a sky can be dark while the
     page stays cream, and stormy is exactly that case. */
  --on-paper: #4a3324;
  --on-paper-soft: #55422f;
  /* Paper laid over the lower painting, as an rgb triplet so the wash
     gradient can vary its alpha. Follows the theme — see the night rule. */
  /* Height of the painted crest at this element's width, and how far it is
     pulled up so its painted peak meets the bear's feet rather than sitting
     below them. Both derive from the artwork's 1600x467 proportions. */
  /*
   * Ground geometry, all derived from the plate. It is 1600x467, so at the
   * box's 150vw width it stands 43.8vw tall (--crest-h), and its painted
   * hilltop begins 9.6vw below its own top edge (--crest-peak) with
   * transparent sky above that.
   *
   * The box is raised by --crest-peak so the painted peak lands at his feet.
   * Shifting the image up inside a fixed box instead would put the box's top
   * edge through the hilltop and slice it off — measure the plate, do not
   * nudge the offset.
   */
  /*
   * Hill geometry. Every landmark is a fraction of the plate's own 1600x467
   * proportions, so they scale together and cannot drift apart:
   *   peak   row 102/467 = 0.219   first painted row
   *   solid  row 213/467 = 0.456   fully opaque from here
   *
   * Height is clamped against the viewport, not just derived from its width.
   * The ground is full-bleed so its natural height follows vw, but the bear is
   * height-driven — on a wide, short window 43.8vw came out taller than the
   * bear himself and the layers stopped nesting.
   */
  --crest-h: min(43.8vw, 32vh);
  --crest-peak: calc(var(--crest-h) * 0.219);
  --crest-solid: calc(var(--crest-h) * 0.456);
  /* Where his soles sit in the artwork, measured from the plates. */
  --feet: 88.9%;
  /* The page the scene is printed on. Everything below the illustration sits
     on this, so text never touches paint and contrast stops depending on the
     weather. */
  --paper: #fdf6e9;
  /* Colour of the halo behind sky-set text — the sky's own tone, so it reads
     as air rather than as a glow. */
  --sky-halo: rgba(232, 244, 251, 0.92);
  /* How far the illustration runs below the crest before the page begins. */
  --band-end: calc(var(--crest-h) * 0.54);
  /* Height of the torn strip between illustration and page. Content has to
     clear all of it, not just the band above it, so a taller fringe costs the
     bear height — 48px keeps the tear clearly visible at a price worth paying. */
  --edge-h: 48px;
  /*
   * The strip's shape — the grass plate's own painted edge, straightened and
   * reused as a mask, from tools/make-edge-mask.py. Lives here rather than on
   * .page so it cannot go missing when that rule is rewritten: an undefined
   * custom property makes the whole mask declaration invalid, and the strip
   * silently renders as a solid rectangle with a dead straight top.
   */
  --deckle: url("../assets/scene/opt/edge_grass.webp");
  /*
   * Clearance so the readout begins below the illustration, not on it.
   *
   * It has to clear --band-end *and* --edge-h: the page proper does not start
   * until after the torn strip, and the top of that strip is still mostly
   * grass. A flat 9vw cleared the band but left the temperature sitting inside
   * the fringe, which is what the overlap was.
   *
   * Tied to the hill so it tracks on every screen. The bear absorbs it — he is
   * the flexible item in the column — so raising it costs his height rather
   * than pushing the readout off the bottom.
   *
   * Sized for the *shortest* he ever gets (his min-height), because the taller
   * he is the less clearance he needs: his box bottom descends faster than the
   * crest does, at a rate of (1 - --feet + --bear-sink) per pixel. Deepening
   * the sink is therefore what buys this number down, which is why it moved
   * from 0.58 to 0.35 when he settled further into the hill.
   */
  --band-clear: calc(var(--crest-h) * 0.35);

  /* Which way he looks. The artwork is drawn facing the viewer's right; -1
     mirrors him to face left. A token rather than a literal so it is one edit
     to turn him round, and so it stays reversible if the artwork is ever
     redrawn the other way. */
  --bear-face: -1;
  /* How far he settles into the grass. A share of his own height rather than a
     fixed offset, so it holds at any screen size — and his box shrinks when the
     forecast tip appears, which a fixed value would not track. Without it he
     perches on the crest like a sticker; with it the blades come up past his
     paws and he is standing in the field.

     It also buys him size. The crest sits at --feet - --bear-sink down his box,
     so sinking him deeper moves the hill up his body without moving it down the
     screen — which lets the box grow into the space the clearance was holding
     open. 7% put him on the brow of the hill with ~100px of dead margin under
     him; 18% stands him behind it and hands most of that margin back. */
  --bear-sink: 18%;
  /* Tint laid over the whole ground. White by day; at night it darkens the
     same plate rather than needing a second one. --veil-top is the strength
     where he stands: nothing by day, but at night the grass under his feet has
     to fall into shadow with the rest of the scene. */
  --veil: 255, 255, 255;
  --veil-top: 0;
  --card: rgba(255, 255, 255, 0.86);
  --card-solid: #fffaf0;
  --cream: #fffaf0;
  --honey: #e89c2e;
  --honey-dark: #c47c12;

  --sky-top: #6fbde8;
  --sky-mid: #b8e0f5;
  --sky-bottom: #e8f4fb;
  --ground: #627d36;
  --ground-img: url("../assets/scene/opt/ground_grass.webp");

  --radius: 20px;
  --radius-lg: 28px;
  /* Warm shadows: a neutral grey one reads as plastic next to watercolour. */
  --shadow: 0 12px 28px rgba(74, 51, 36, 0.14);
  --shadow-soft: 0 4px 12px rgba(74, 51, 36, 0.09);

  --tap: 56px;
}

* { box-sizing: border-box; }

.filters { position: absolute; width: 0; height: 0; overflow: hidden; }

/*
 * Paper tooth over the whole page. This is what stops the code-drawn scenery
 * and the painted bear looking like two different materials — they end up
 * sharing one surface. Kept very faint: at any strength you can consciously
 * see, it reads as noise rather than paper.
 */
.grain {
  position: fixed;
  inset: -50%;
  z-index: 2;
  pointer-events: none;
  opacity: 0.16;
  mix-blend-mode: multiply;
  filter: url(#grain);
}

/* Line icons, drawn in the current text colour so they belong to the palette. */
.icon {
  width: 1.05em;
  height: 1.05em;
  flex: 0 0 auto;
  vertical-align: -0.17em;
  stroke-linecap: round;
}

html, body { height: 100%; }

body {
  margin: 0;
  font-family: var(--font);
  color: var(--ink);
  background: var(--sky-bottom);
  -webkit-text-size-adjust: 100%;
  -webkit-tap-highlight-color: transparent;
  /*
   * Pinned rather than merely clipped.
   *
   * `overflow: hidden` stopped the decoration making the document scrollable,
   * but on iOS it created a worse bug: focusing the city field scrolls the page
   * up to make room for the keyboard, and with overflow hidden it can never
   * scroll back — the topbar and the bear's head stay off-screen for good.
   * Fixing the body to the viewport means there is nothing to scroll in the
   * first place.
   */
  position: fixed;
  inset: 0;
  overflow: hidden;
  display: flex;
  flex-direction: column;
  /*
   * dvh, not 100% — and this is the whole of the "pills are cut off" bug.
   *
   * `inset: 0` on a fixed element resolves against the initial containing
   * block, which on iOS Safari is the LARGE viewport: the size the page would
   * be with the toolbars hidden. With the toolbar showing, an iPhone 17 is 811
   * points of containing block over 724 points of visible glass, so the footer
   * rendered ~90px underneath Safari's own chrome and everything above it was
   * squeezed. dvh tracks the visible area instead, shrinking and growing as the
   * toolbar does. The `bottom` from `inset` is over-constrained once height is
   * set and is ignored, which is what we want; it stays as the fallback for
   * anything without dvh support.
   */
  height: 100dvh;
  padding:
    env(safe-area-inset-top)
    env(safe-area-inset-right)
    env(safe-area-inset-bottom)
    env(safe-area-inset-left);
}

/* ============================================================ sky backdrop */

/*
 * Sky is a gradient; everything on it is painted. A gradient reads perfectly
 * well as sky, and painting it would mean shipping six versions of it — but a
 * cloud drawn as geometry sits next to a watercolour bear as an obvious
 * cut-out, which is what these replace.
 *
 * Nothing painted is filtered. The wash filters that roughed up the old vector
 * shapes would only smear real paint.
 */
.sky {
  position: fixed;
  inset: 0;
  z-index: -1;
  background: linear-gradient(to bottom,
    var(--sky-top) 0%,
    var(--sky-mid) 52%,
    var(--sky-bottom) 100%);
  transition: background 900ms ease;
  overflow: hidden;
}

/* ------------------------------------------------------------- sun & moon */

.orb {
  position: absolute;
  top: 5%;
  right: 8%;
  width: min(34vw, 170px);
  height: auto;
  opacity: 0;
  transition: opacity 900ms ease;
}

/* -------------------------------------------------------------- the clouds */

.cloud {
  position: absolute;
  width: 210px;
  opacity: 0;
  transition: opacity 900ms ease;
  animation: drift linear infinite;
  will-change: transform;
}

.cloud img {
  display: block;
  width: 100%;
  height: auto;
  transition: opacity 700ms ease;
}

/* Both weights live in the DOM and cross-fade, so a change of weather does not
   pop a new image in mid-drift. */
.cloud__heavy {
  position: absolute;
  inset: 0;
  opacity: 0;
}

/* Three depths. The big one is nearest and moves fastest; the small one sits
   far back and barely crawls, which is what sells the parallax. */
.cloud--a { width: 210px; top: 13%; left: -30%; animation-duration: 62s; }
.cloud--b { width: 160px; top: 27%; left: -26%; animation-duration: 88s;  animation-delay: -30s; }
.cloud--c { width: 130px; top:  6%; left: -38%; animation-duration: 115s; animation-delay: -60s; }

@keyframes drift { from { transform: translateX(0); } to { transform: translateX(210vw); } }

/* Rain and snow are one repeating-gradient layer scrolled downward. */
.sky__weather {
  position: absolute;
  inset: -20% -10% -10%;
  opacity: 0;
  transition: opacity 700ms ease;
  pointer-events: none;
}

body[data-sky="sunny"] { --sky-top: #55b3e6; --sky-mid: #a5d9f2; --sky-bottom: #e9f5fc; --ground: #627d36; --ground-img: url("../assets/scene/opt/ground_grass.webp"); }
body[data-sky="sunny"] .orb--sun { opacity: 1; }
body[data-sky="sunny"] .cloud--a { opacity: 0.95; }
body[data-sky="sunny"] .cloud--c { opacity: 0.7; }

body[data-sky="cloudy"] { --sky-top: #9fb4c4; --sky-mid: #c9d7e0; --sky-bottom: #e9eef2; --ground: #627d36; --ground-img: url("../assets/scene/opt/ground_grass.webp"); }
body[data-sky="cloudy"] .cloud { opacity: 1; }

body[data-sky="rainy"] { --sky-top: #9db1c0; --sky-mid: #a9bcc9; --sky-bottom: #d8e2e8; --ground: #627d36; --ground-img: url("../assets/scene/opt/ground_grass.webp"); }
body[data-sky="rainy"] .cloud { opacity: 1; }
body[data-sky="rainy"] .cloud__soft { opacity: 0; }
body[data-sky="rainy"] .cloud__heavy { opacity: 1; }
body[data-sky="rainy"] .sky__weather {
  opacity: 0.85;
  background-image:
    url("../assets/scene/opt/rain_near.webp"),
    url("../assets/scene/opt/rain_far.webp");
  background-size: 420px 640px, 290px 440px;
  animation: rainfall 1.15s linear infinite;
}

body[data-sky="stormy"] { --on-sky: #f4efe6; --sky-halo: rgba(60, 74, 88, 0.92); --sky-top: #556674; --sky-mid: #5e6c7a; --sky-bottom: #c3cdd4; --ground: #627d36; --ground-img: url("../assets/scene/opt/ground_grass.webp"); }
body[data-sky="stormy"] .cloud { opacity: 1; }
body[data-sky="stormy"] .cloud__soft { opacity: 0; }
body[data-sky="stormy"] .cloud__heavy { opacity: 1; }
body[data-sky="stormy"] .sky__weather {
  opacity: 1;
  background-image:
    url("../assets/scene/opt/rain_near.webp"),
    url("../assets/scene/opt/rain_far.webp");
  background-size: 420px 640px, 290px 440px;
  animation: rainfall 0.85s linear infinite;
}

body[data-sky="snowy"] { --sky-top: #b7c8d6; --sky-mid: #d8e4ed; --sky-bottom: #f1f6fa; --ground: #cadaed; --ground-img: url("../assets/scene/opt/ground_snow.webp"); --crest-peak: 10.9vw; }
body[data-sky="snowy"] .cloud { opacity: 0.8; }
body[data-sky="snowy"] .sky__weather {
  opacity: 0.95;
  background-image:
    url("../assets/scene/opt/snow_near.webp"),
    url("../assets/scene/opt/snow_far.webp");
  background-size: 420px 640px, 280px 430px;
  animation: snowfall 14s linear infinite;
}

body[data-sky="foggy"] { --sky-top: #b3bcc1; --sky-mid: #ccd3d6; --sky-bottom: #e7eaeb; --ground: #627d36; --ground-img: url("../assets/scene/opt/ground_grass.webp"); }
body[data-sky="foggy"] .sky__weather {
  opacity: 0.75;
  background: linear-gradient(
    to bottom,
    rgba(255, 255, 255, 0) 0%,
    rgba(255, 255, 255, 0.7) 45%,
    rgba(255, 255, 255, 0.35) 100%
  );
}

body[data-sky="night"] { --sky-top: #2f4664; --sky-mid: #4d6684; --sky-bottom: #8098b3; --ground: #627d36; --ground-img: url("../assets/scene/opt/ground_grass.webp"); }
body[data-sky="night"] { --on-sky: #f4efe6; --on-paper: #f4efe6; --on-paper-soft: #ccd8e4; --paper: #1f2b3d; --veil: 18, 28, 44; --veil-top: 0.42; --sky-halo: rgba(30, 44, 66, 0.92); }
body[data-sky="night"] .cloud { opacity: 0.45; filter: brightness(0.62) saturate(0.7); }
body[data-sky="night"] .orb--moon { opacity: 1; }

@keyframes rainfall {
  from { background-position: 0 0, 0 0; }
  to   { background-position: 148px 640px, 102px 440px; }
}

@keyframes snowfall {
  from { background-position: 0 0, 0 0; }
  to   { background-position: 46px 640px, -34px 430px; }
}

/* ================================================================== topbar */

.topbar {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 10px 12px 4px;
}

.topbar__place,
.topbar__settings {
  font: inherit;
  color: var(--ink);
  background: var(--card);
  border: none;
  border-radius: 999px;
  box-shadow: var(--shadow-soft);
  cursor: pointer;
  min-height: 48px;
  display: inline-flex;
  align-items: center;
  gap: 6px;
  transition: transform 120ms ease, background 200ms ease;
}

.topbar__place {
  flex: 1 1 auto;
  min-width: 0;
  padding: 0 16px;
  font-size: 1.05rem;
  font-weight: 700;
}

.topbar__placeName {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.topbar__settings {
  flex: 0 0 auto;
  width: 48px;
  justify-content: center;
  font-size: 1.25rem;
}

.topbar__place:active,
.topbar__settings:active { transform: scale(0.96); }

/* =================================================================== stage */

/*
 * One screen, no scrolling, on a phone. The bear takes whatever vertical space
 * the fixed-height pieces leave over, so he is the largest thing on the page on
 * every handset rather than at one particular size.
 */
.stage {
  flex: 1 1 auto;
  min-height: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 8px;
  padding: 2px 18px 4px;
  width: 100%;
  max-width: 520px;
  margin: 0 auto;
  text-align: center;
}

/* -------------------------------------------------------------- the moment */

.moment {
  margin: 0;
  padding: 8px 16px;
  border-radius: 999px;
  background: linear-gradient(105deg, #ffd977, #ffb84d);
  color: #5a3c12;
  font-weight: 700;
  font-size: 0.95rem;
  line-height: 1.3;
  box-shadow: var(--shadow-soft);
  animation: pop 500ms cubic-bezier(0.2, 1.4, 0.5, 1) both;
}

@keyframes pop {
  from { transform: scale(0.85); opacity: 0; }
  to   { transform: scale(1);    opacity: 1; }
}

/* -------------------------------------------------------- the speech bubble */

/*
 * The bear's line, set straight into the sky.
 *
 * It was a cream card with a tail — the last UI container left sitting on the
 * illustration, and a tail is a drawn triangle, which is the same vector
 * problem as everything else. Taking the container away leaves the words and
 * the scene, which is how a picture book sets narration over a painting.
 *
 * It costs the explicit "he is speaking" signal a balloon gives. The copy is
 * written in his voice and first person, and he is directly below it, so the
 * connection survives — but it is a real trade, not a free win.
 */
.bubble {
  position: relative;
  width: 100%;
  flex: 0 0 auto;
  padding: 4px 6px 2px;
}

.bubble__text {
  margin: 0;
  font-size: clamp(1.3rem, 5.6vw, 1.7rem);
  font-weight: 600;
  line-height: 1.32;
  text-wrap: balance;
  color: var(--on-sky);
  /* A soft halo of the sky's own colour, so a cloud drifting behind a line
     never takes its legibility with it. */
  text-shadow: 0 1px 10px var(--sky-halo), 0 0 22px var(--sky-halo);
  transition: color 900ms ease;
}

/* ------------------------------------------------------------------- bear */

.bear {
  /*
   * A declared height, not a flexible one.
   *
   * He used to be `flex: 1 1 auto` — the one item that absorbed whatever the
   * column had left — which meant his size was decided by how wordy the
   * forecast was. A long message and a four-pill outfit shrank him to his
   * min-height and then pushed the rest off the screen. Now he is sized
   * against the viewport and the readout scrolls, so neither can take from
   * the other.
   *
   * clamp, not a plain vh: the floor keeps him readable on a short handset and
   * the ceiling stops him swallowing a tall one. The floor is deliberately
   * below the 28vh a normal phone resolves to — on a 553pt screen (an SE with
   * Safari's bars up) 190px left the readout 51px tall, which is a temperature
   * and nothing else. It only binds down there; at 724pt and 874pt the vh term
   * wins and he is 203px and 245px regardless.
   */
  flex: 0 0 auto;
  height: clamp(165px, 28vh, 330px);
  /* The illustration band runs past the bottom of this box, so the readout
     needs clearance or it lands on grass. */
  margin-bottom: var(--band-clear);
  width: 100%;
  display: flex;
  align-items: flex-end;
  justify-content: center;
  position: relative;
}

/*
 * The ground he stands on. Anchored to the bear rather than to the viewport:
 * his feet sit ~11% up from the bottom of the square canvas, so a percentage
 * offset here keeps the horizon at his feet on any screen, where a fixed
 * offset would drift on every handset.
 *
 * Full-bleed past the stage's max-width, because a ground that stops at a
 * 520px column is a rug, not a landscape.
 */
.bear::before {
  content: "";
  position: absolute;
  z-index: 0;
  /*
   * `top`, not `bottom`: anchoring the bottom edge at his feet would make the
   * block extend upward and paint over the whole sky. His soles sit at --feet
   * down the artwork, less the distance he settles into the grass, less the
   * transparent sky above the crest in the ground plate.
   */
  top: calc(var(--feet) - var(--bear-sink) - var(--crest-peak));
  left: 50%;
  transform: translateX(-50%);
  width: 150vw;
  /* Runs off the bottom of the screen; the page half has no natural end. */
  height: 100vh;

  /*
   * Illustration above, page below, drawn as one element so the two can never
   * drift apart.
   *
   * Three fades stacked on different origins is what produced the colour break
   * here before — the fill measured against its own height, the veil against
   * this box, the page wash against the viewport, and their rates disagreed.
   * One element, one origin, and a deliberate edge instead of a blend.
   */
  background-image:
    /*
     * Mood tint over the illustration only — night, mostly.
     *
     * It has to start where the plate turns opaque, not at the top of this
     * box. The plate is transparent sky above the hilltop, so a tint starting
     * at 0 darkens the sky itself and shows as a bar straight across it. The
     * flat ground colour below had the same fault and was fixed; this layer
     * was missed. Ramped between --crest-peak and --crest-solid to follow the
     * paint's own fade-in rather than replacing one hard edge with another.
     */
    linear-gradient(to bottom,
      rgba(var(--veil), 0) 0 var(--crest-peak),
      rgba(var(--veil), var(--veil-top)) var(--crest-solid) var(--band-end),
      rgba(var(--veil), 0) var(--band-end)),
    /* The painted crest. */
    var(--ground-img),
    /* Flat grass behind the crest, starting only where the plate turns solid
       so it does not fill the transparent sky above the hilltop. */
    linear-gradient(to bottom,
      transparent 0 var(--crest-solid),
      var(--ground) var(--crest-solid) 100%);
  background-repeat: no-repeat, no-repeat, no-repeat;
  background-position: top center, top center, top center;
  background-size: 100% 100%, 100% var(--crest-h), 100% 100%;
  transition: background 900ms ease;
}

/*
 * The page the readout sits on, and the torn edge where the land becomes it.
 *
 * A straight cut read as abrupt against watercolour, so the top edge is masked
 * with an irregular deckle — the land tears away into paper rather than being
 * sliced. Masked rather than drawn, so --paper still carries the colour and
 * the edge follows the theme at night.
 *
 * Its own element rather than another layer inside ::before, because a mask
 * applies to a whole element: masking the shared one would have cut the
 * illustration too.
 */
.page {
  position: absolute;
  z-index: 1;
  /* Starts one edge-height lower; ::before draws the torn strip above it. */
  top: calc(var(--feet) - var(--bear-sink) - var(--crest-peak)
            + var(--band-end) + var(--edge-h));
  left: 50%;
  transform: translateX(-50%);
  width: 150vw;
  height: 100vh;
  background: var(--paper);
  transition: background 900ms ease;
}

/*
 * The torn strip, as its own element with a single-layer mask.
 *
 * It was one element with two mask layers — the deckle plus a calc()-sized
 * solid — which iOS Safari resolves to an empty mask, hiding the page
 * completely and leaving the green ground showing. One image, one size, no
 * calc, and it composites everywhere.
 */
.page::before {
  content: "";
  position: absolute;
  /* Overlapped by a pixel. Sitting exactly on .page's top edge leaves a
     sub-pixel hairline between two areas of the same colour. */
  bottom: calc(100% - 1px);
  left: 0;
  right: 0;
  height: var(--edge-h);
  background: var(--paper);
  -webkit-mask: var(--deckle) top center / 600px var(--edge-h) repeat-x;
  mask: var(--deckle) top center / 600px var(--edge-h) repeat-x;
}


/* Contact shadow, so he is planted on the ground rather than pasted over it. */
.bear::after {
  content: "";
  position: absolute;
  z-index: 2;
  bottom: 10%;
  left: 50%;
  transform: translateX(-50%);
  width: 34%;
  height: 16px;
  border-radius: 50%;
  background: radial-gradient(ellipse, rgba(60, 46, 30, 0.28) 0%, rgba(60, 46, 30, 0) 70%);
}

/*
 * The artwork is a square canvas with the bear occupying the middle ~41% of its
 * width and ~76% of its height, so the box has to be oversized for the bear
 * himself to read as large. Height-driven rather than width-driven, so he grows
 * into a tall screen instead of overflowing a narrow one.
 */
.bear__img {
  position: relative;
  z-index: 3;
  height: 100%;
  width: auto;
  max-width: min(96vw, 420px);
  aspect-ratio: 1 / 1;
  object-fit: contain;
  filter: drop-shadow(0 10px 14px rgba(74, 51, 36, 0.16));
  /* Held still. He is standing in a field, and a bear that drifts up and down
     reads as floating rather than alive — especially now that he has ground
     under him and a contact shadow to stay attached to. */
  transform: scaleX(var(--bear-face));
}

/* Cross-fade whenever the outfit changes. */
.bear__img.is-swapped { animation: fadeIn 450ms ease both; }

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

/*
 * Everything that is not scenery has to sit above the ground and the page.
 *
 * The bubble and the moment were left out of this when the page only ever
 * covered the area *below* the bear — nothing could overlap them. In landscape
 * the page is a right-hand panel spanning the full height, so it painted over
 * the bear's line and the line simply disappeared.
 * `.bear::before` is a positioned element, so without this it paints over its
 * own static siblings and the whole readout vanishes behind the grass.
 */
.moment,
.bubble,
.readout,
.reading,
.outfit,
.tip,
.notes,
.alert,
.footer {
  position: relative;
  /* Above .page, which is 1. Equal values would not be enough: .page lives
     inside .bear, which comes after these in the DOM, so it would win the tie
     and paint over them. */
  z-index: 2;
}

/* ---------------------------------------------------------------- readout */

/*
 * The paper half, and the only thing on the page that scrolls.
 *
 * The body is deliberately `position: fixed` (see above — it is what stops the
 * iOS keyboard scrolling the topbar away for good), so the scroll has to happen
 * in a box inside it rather than on the document. This is that box: the bear
 * and his hill stay put, the text moves.
 *
 * min-height: 0 is load-bearing. A flex item's default minimum is its content,
 * so without it this box refuses to shrink below the full height of the readout
 * and overflows the stage instead of scrolling — which is the bug it is here to
 * fix, just moved down one level.
 */
.readout {
  flex: 1 1 auto;
  min-height: 0;
  width: 100%;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 8px;
  overflow-y: auto;
  /* So the last line can be scrolled clear of the fade rather than ending
     underneath it. */
  padding-bottom: 24px;
  /* Don't hand the scroll to the page behind when this one reaches its end;
     on iOS that shows as the whole app rubber-banding. */
  overscroll-behavior: contain;
}

/* ---------------------------------------------------------------- reading */

/*
 * Temperature and sky on one baseline. Stacked, they read as three separate
 * facts of equal weight; side by side they read as one sentence, and the row
 * costs half the height — which goes to the bear.
 */
.reading {
  flex: 0 0 auto;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0;
}

.reading__line {
  display: flex;
  align-items: baseline;
  justify-content: center;
  gap: 10px;
  flex-wrap: wrap;
}

.reading__temp {
  margin: 0;
  font-size: clamp(2.7rem, 12.5vw, 3.6rem);
  font-weight: 800;
  line-height: 1.05;
  letter-spacing: -0.02em;
  color: var(--on-paper);
  text-shadow: 0 2px 0 rgba(255, 255, 255, 0.35);
}

.reading__desc {
  margin: 0;
  font-size: clamp(1rem, 4.2vw, 1.15rem);
  font-weight: 600;
  color: var(--on-paper-soft);
  text-transform: lowercase;
}

.reading__meta {
  margin: 0;
  font-size: 0.85rem;
  font-weight: 500;
  color: var(--on-paper-soft);
  opacity: 0.9;
}

/* ---------------------------------------------------------------- outfit */

/*
 * No card. The chips sit straight on the sky — a container around them turns a
 * short list into a panel, and the page already had four of those.
 */
.outfit {
  flex: 0 0 auto;
  width: 100%;
}

.outfit__heading {
  margin: 0 0 6px;
  font-size: 0.95rem;
  font-weight: 700;
  color: var(--on-paper-soft);
  /* Sentence case, not letterspaced caps: this is a bear talking, not a
     dashboard section label. */
  text-transform: none;
  letter-spacing: 0;
}

.outfit__list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: 7px;
}

.outfit__list li {
  display: inline-flex;
  align-items: center;
  gap: 7px;
  padding: 7px 14px;
  border-radius: 999px;
  background: rgba(255, 255, 255, 0.8);
  box-shadow: var(--shadow-soft);
  font-size: 0.98rem;
  font-weight: 600;
  line-height: 1.2;
  color: var(--ink);
}

/* A small honey dot instead of an emoji: it gives the row rhythm without
   dropping flat vector clip-art next to a watercolour bear. */
.outfit__list li::before {
  content: "";
  width: 7px;
  height: 7px;
  border-radius: 50%;
  background: var(--honey);
  flex: 0 0 auto;
}

/* ------------------------------------------------------------ tip & notes */

.tip {
  flex: 0 0 auto;
  width: 100%;
  padding: 11px 16px;
  border-radius: var(--radius);
  background: linear-gradient(160deg, rgba(255, 246, 224, 0.95), rgba(255, 236, 198, 0.95));
  box-shadow: var(--shadow-soft);
}

.tip__heading {
  margin: 0 0 3px;
  font-size: 0.8rem;
  font-weight: 700;
  color: var(--honey-dark);
  text-transform: uppercase;
  letter-spacing: 0.07em;
}

.tip__list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 5px;
}

.tip__list li {
  font-size: 1.02rem;
  font-weight: 600;
  line-height: 1.32;
  color: var(--ink);
  text-wrap: balance;
}

/* The smallest voice on the page — true, but never the point. */
.notes {
  flex: 0 0 auto;
  width: 100%;
}

.notes__list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 3px;
}

.notes__list li {
  font-size: 0.9rem;
  font-weight: 500;
  line-height: 1.35;
  color: var(--on-paper-soft);
  text-wrap: balance;
}

/* ----------------------------------------------------------------- alert */

.alert {
  width: 100%;
  background: var(--card-solid);
  border-radius: var(--radius);
  box-shadow: var(--shadow);
  padding: 16px;
}

.alert__text {
  margin: 0 0 12px;
  font-size: 1.05rem;
  font-weight: 700;
  line-height: 1.4;
}

.alert__actions { display: flex; flex-wrap: wrap; gap: 10px; justify-content: center; }

/* ================================================================= footer */

.footer {
  padding: 2px 16px 8px;
  display: flex;
  justify-content: center;
  /*
   * Opaque, and the same paper the page is printed on, so it reads as one
   * surface rather than a bar.
   *
   * The refresh pill is translucent, and the readout now scrolls directly
   * behind this strip — so without a background the tip card's last line stayed
   * half-legible *through* the pill, which looks exactly like the overlap bug
   * this was meant to fix. The fade above handles the hand-off.
   */
  background: var(--paper);
}

/*
 * A short fade above the footer, so a readout that continues below the fold
 * says so. Without it the scroller just ends on a clean line and the forecast
 * tip looks like it is simply absent.
 *
 * On .footer rather than .readout because it has to sit *over* the scrolling
 * text, and an ::after inside the scroller would scroll away with it.
 */
.footer::before {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  bottom: 100%;
  height: 28px;
  pointer-events: none;
  /*
   * `transparent` is transparent *black*, so a fade from it to a dark --paper
   * interpolates through grey and draws a visible light bar instead of a fade —
   * which is what it did on the night theme. Fading from --paper at zero alpha
   * keeps the hue and only moves the opacity.
   *
   * Relative colour syntax, so this tracks whatever --paper the theme sets
   * rather than needing a second token per sky. Browsers without it drop the
   * declaration and simply get no fade, which is the right way to lose.
   */
  background: linear-gradient(
    to bottom,
    rgb(from var(--paper) r g b / 0) 0%,
    var(--paper) 100%);
}

.footer__refresh {
  font: inherit;
  font-size: 0.92rem;
  font-weight: 700;
  color: var(--ink-soft);
  background: rgba(255, 255, 255, 0.6);
  border: none;
  border-radius: 999px;
  padding: 0 18px;
  min-height: 44px;
  cursor: pointer;
}

.footer__refresh:active { transform: scale(0.97); }
.footer__refresh[disabled] { opacity: 0.55; }

/* ================================================================ buttons */

.btn {
  font: inherit;
  font-size: 1.05rem;
  font-weight: 800;
  color: var(--ink);
  background: #fff;
  border: 2px solid rgba(67, 48, 31, 0.12);
  border-radius: 999px;
  min-height: var(--tap);
  padding: 0 22px;
  cursor: pointer;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  transition: transform 120ms ease, filter 160ms ease;
}

.btn:active { transform: scale(0.97); }
.btn--block { width: 100%; }

.btn--primary {
  background: linear-gradient(180deg, var(--honey), var(--honey-dark));
  border-color: transparent;
  color: #fff;
  text-shadow: 0 1px 1px rgba(0, 0, 0, 0.18);
}

/* ================================================================= sheets */

.sheet {
  border: none;
  border-radius: 26px 26px 0 0;
  padding: 0;
  width: 100%;
  max-width: 520px;
  margin: auto auto 0;
  background: var(--card-solid);
  color: var(--ink);
  box-shadow: 0 -8px 40px rgba(0, 0, 0, 0.25);
}

.sheet::backdrop { background: rgba(43, 31, 20, 0.45); backdrop-filter: blur(2px); }

.sheet[open] { animation: slideUp 260ms cubic-bezier(0.2, 0.9, 0.3, 1) both; }

@keyframes slideUp { from { transform: translateY(24px); opacity: 0; } to { transform: translateY(0); opacity: 1; } }

.sheet__form {
  display: flex;
  flex-direction: column;
  gap: 12px;
  padding: 22px 20px calc(22px + env(safe-area-inset-bottom));
}

.sheet__title {
  margin: 0 0 4px;
  font-size: 1.4rem;
  font-weight: 800;
  text-align: center;
}

.sheet__or {
  display: flex;
  align-items: center;
  gap: 10px;
  color: var(--ink-soft);
  font-size: 0.9rem;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.08em;
}

.sheet__or::before,
.sheet__or::after {
  content: "";
  flex: 1;
  height: 2px;
  background: rgba(67, 48, 31, 0.12);
  border-radius: 2px;
}

.sheet__status { margin: 0; font-size: 0.98rem; font-weight: 600; color: var(--ink-soft); min-height: 1.2em; }
.sheet__note { margin: 0; font-size: 0.9rem; color: var(--ink-soft); text-align: center; }

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

.field__label {
  font-size: 0.9rem;
  font-weight: 800;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--ink-soft);
}

.field__input {
  font: inherit;
  font-size: 1.15rem;
  font-weight: 600;
  color: var(--ink);
  background: #fff;
  border: 2px solid rgba(67, 48, 31, 0.16);
  border-radius: 16px;
  min-height: var(--tap);
  padding: 0 16px;
  width: 100%;
}

.field__input:focus-visible { outline: 3px solid var(--honey); outline-offset: 1px; }

/* Search results — each row is a full-width tap target. */
.results { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 8px; }

.results:empty { display: none; }

.results button {
  font: inherit;
  font-size: 1.05rem;
  font-weight: 700;
  text-align: left;
  width: 100%;
  min-height: var(--tap);
  padding: 0 16px;
  color: var(--ink);
  background: #fff;
  border: 2px solid rgba(67, 48, 31, 0.12);
  border-radius: 16px;
  cursor: pointer;
}

.results button:active { transform: scale(0.98); }

/* ------------------------------------------------------------- segmented */

.segmented { border: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 6px; }

.segmented__row {
  display: flex;
  gap: 6px;
  background: rgba(67, 48, 31, 0.08);
  border-radius: 999px;
  padding: 5px;
}

.segmented__btn {
  font: inherit;
  flex: 1;
  font-size: 1.1rem;
  font-weight: 800;
  color: var(--ink-soft);
  background: transparent;
  border: none;
  border-radius: 999px;
  min-height: 48px;
  cursor: pointer;
  transition: background 160ms ease, color 160ms ease;
}

.segmented__btn[aria-pressed="true"] {
  background: #fff;
  color: var(--ink);
  box-shadow: var(--shadow-soft);
}

/* ============================================================== a11y bits */

:focus-visible { outline: 3px solid var(--honey-dark); outline-offset: 2px; }

[hidden] { display: none !important; }

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.001ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.001ms !important;
  }
}

/* Wider screens: put the bear beside its speech bubble. */
/*
 * Wider screens keep the bear beside his line, but everything else drops below
 * the horizon.
 *
 * This layout used to put the readout in the left column alongside him, which
 * was fine when the background was a gradient. Once the ground became a
 * full-bleed band at his feet, that column sat on grass — dark text on
 * painting, exactly the problem the page was introduced to remove. Only the
 * sky-set line may share a row with him now.
 */
@media (min-width: 760px) and (min-height: 560px) {
  /* This layout places the sections as named grid areas, which only works if
     they are grid items — so the scroller stops generating a box. Scrolling is
     a portrait-phone measure; there is room here. */
  .readout { display: contents; }

  /* Nothing scrolls here, so there is nothing to hint at. */
  .footer::before { display: none; }

  .stage {
    /*
     * Clearance below the horizon. The mobile value is a share of viewport
     * width, which is far too little here: the band hangs roughly 63px below
     * the bear's box on a wide window, and 9vw barely cleared it, so the
     * temperature sat right against the torn edge. Tied to the hill instead,
     * since that is what it has to clear.
     */
    /*
     * Sized against the budget, not picked by eye. At 1100x820 the stage has
     * ~696px; the readout, chips, tip and notes need ~208 of it with gaps, so
     * the bear and his clearance have to fit in ~488. Overshooting does not
     * scroll — it silently spills the chips out of the stage and the footer
     * lands on top of them, which is exactly what 0.42 had started doing here
     * once the bear sank deeper into the hill: the clearance a given bear needs
     * falls as he grows, because his box bottom drops faster than the crest
     * does. At this size he needs about 34px and was being given 110.
     */
    --band-clear: calc(var(--crest-h) * 0.26);

    max-width: 900px;
    display: grid;
    grid-template-columns: 1fr 1fr;
    grid-template-areas:
      "moment moment"
      "bubble bear"
      "reading reading"
      "outfit  tip"
      "notes   notes"
      "alert   alert";
    align-items: center;
    gap: 10px 24px;
    padding-top: 10px;
  }
  .moment  { grid-area: moment; justify-self: center; }

  /* Sits at the top of the row rather than centred in it. Centred, it floated
     halfway down a tall row and read as detached from him. */
  .bubble  { grid-area: bubble; align-self: start; padding-top: 2vh; }

  /* An explicit height, because `flex: 1` does nothing to a grid item — the row
     was sizing to the bubble and capping him well short of the space available. */
  .bear    { grid-area: bear; height: min(41vh, 420px); }
  .reading { grid-area: reading; align-self: start; }
  .outfit  { grid-area: outfit; align-self: start; }
  .tip     { grid-area: tip; align-self: start; }
  .notes   { grid-area: notes; }
  .alert   { grid-area: alert; }

  /* Height-driven here too, so he stays the tallest thing in the scene and his
     feet keep meeting the horizon the ground is positioned from. */
  .bear__img {
    height: 100%;
    width: auto;
    max-width: min(46vw, 470px);
  }
}

/*
 * Landscape on a phone: wide, and far too short to stack.
 *
 * The two-column layout above asks for 560px of height, which a landscape
 * handset does not have, so this used to fall back to the portrait stack in
 * ~400px — the bear took what little height there was and the entire readout
 * was pushed off the bottom and clipped. No temperature, no outfit, no tip.
 *
 * So the split turns ninety degrees: illustration on the left with the bear at
 * full height, page down the right with everything else on it. Same idea as
 * portrait — paint on one side, paper on the other — just along the other axis.
 */
@media (orientation: landscape) and (max-height: 560px) and (min-width: 640px) {
  /*
   * Unlike the wide layout, here the scroller stays a real box — it *is* the
   * right-hand column.
   *
   * Listing the sections as their own grid rows assumed they would always fit,
   * and in 402px of height they do not: the tip card and the notes line ran
   * straight through the footer and off the bottom. One scrolling area instead,
   * the same answer as portrait.
   */
  .readout {
    grid-area: readout;
    /* The row is 1fr but the grid aligns items to `start`, so without this it
       shrinks to its content and never scrolls. */
    align-self: stretch;
    display: flex;
    flex-direction: column;
    align-items: stretch;
    gap: 4px;
    min-height: 0;
    overflow-y: auto;
    overscroll-behavior: contain;
  }

  .stage {
    max-width: none;
    display: grid;
    grid-template-columns: minmax(0, 1fr) minmax(0, 1.15fr);
    grid-template-areas:
      "bear moment"
      "bear bubble"
      "bear readout";
    grid-template-rows: auto auto 1fr;
    align-items: start;
    gap: 4px 20px;
    padding: 2px 16px 4px;
  }

  .moment  { grid-area: moment; justify-self: start; }
  .bubble  { grid-area: bubble; padding-top: 0; }
  .bear    { grid-area: bear; height: 100%; margin-bottom: 0; align-self: stretch; }

  /* The readout does scroll here, but the footer runs the full width while only
     its right half is paper — so neither the fade nor an opaque paper strip can
     span it without covering the grass. The clipped edge of the tip card is hint
     enough. */
  .footer::before { display: none; }
  .footer { background: transparent; }

  .bear__img { max-width: min(44vw, 340px); }

  /* Text sits left in this column rather than centred under nothing. */
  .stage .reading,
  .stage .outfit__list { align-items: flex-start; }
  .reading__line,
  .outfit__list { justify-content: flex-start; }
  .bubble__text,
  .reading__meta,
  .outfit__heading,
  .notes__list li,
  .tip__list li { text-align: left; }

  .bubble__text { font-size: clamp(1rem, 2.6vw, 1.25rem); }
  .reading__temp { font-size: clamp(2rem, 5vw, 2.6rem); }

  /* The page becomes a right-hand panel. Its edge is a soft feather rather
     than the torn deckle — that mask strip is horizontal, and rotating it for
     one orientation is more fragility than the detail is worth. */
  .page {
    /*
     * Fixed, not absolute. Its containing block is .bear, which in this layout
     * is the left column — so `right: 0` meant the bear's right edge and the
     * panel landed in the middle of the screen with grass showing beneath the
     * text. Pinning it to the viewport is what "right-hand panel" actually
     * means here.
     */
    position: fixed;
    top: 0;
    bottom: 0;
    left: auto;
    right: 0;
    transform: none;
    width: 54vw;
    height: auto;
    -webkit-mask-image: linear-gradient(to right, transparent 0, #000 22px);
    mask-image: linear-gradient(to right, transparent 0, #000 22px);
  }
  .page::before { display: none; }

  .footer { padding: 0 16px 6px; }
}
