Day / Night Hillside how it’s made

The idea

A paper-cut hillside town sits under a golden-hour sky. A pill-shaped switch at the bottom says "Night". Flip it and, over 3.6 seconds, the sun drops behind the horizon while a crescent moon rises, the sky and every hill and roof cross-fade to a night palette, a warm haze slides along the horizon, stars fade in after the sky darkens, and the windows light up one by one. Flip it back and it all reverses. One HTML file, one CSS file, no JavaScript. The whole state is one checkbox.

Techniques

Checkbox as preceding sibling (no :has())

The <input id="night"> is the first child of .stage, before .world and the label. Every night-state rule is a plain sibling selector:

#night:checked ~ .world {
  --c-sky-top: #0e1230; --c-sky-low: #3a2a5c; --c-glow: #6b5aa8;

Because the checkbox precedes everything it styles, ~ is enough. The brief reached for :has(); this deviates on purpose. :has() would let the checkbox live anywhere, but costs a wider invalidation scope and gains nothing here. Trade-off: the checkbox must stay before its targets in source order. The label (for="night") toggles it, so keyboard and click both work. Support: universal.

Typed @property palette crossfade

Nine colours are registered as <color>, so a change to them interpolates instead of snapping:

@property --c-sky-top  { syntax: "<color>"; inherits: true; initial-value: #5aa9e6; }

.world lists all nine in transition-property, and the night rule only reassigns them. Everything downstream (fill: var(--c-far-hill), the sky gradient) follows for free, with one transition on one container and the same --ease-out curve as the orbit so colour tracks the sun.

Cost, stated in the source as paint-ok: this is a main-thread transition. Each frame the inheriting properties restyle every descendant and repaint the SVG fills and the full-size sky gradient. It is flat fills with no blur, but it is not compositor work. Support: @property is Baseline newly available (2024-07-09). Without it the palette snaps at once to the night values; the scene still works.

One pivot rotation swaps sun and moon

Sun and moon are children of a zero-size pivot on the horizon, one placed --r above it and one --r below:

#night:checked ~ .world .orbit { rotate: 180deg; }

Rotating the pivot by 180deg sends the sun down and brings the moon up along the same arc, with a single rotate transition. rotate is an individual transform, so on its own it is a compositor-friendly property. The horizon landscape hides whichever body is below it. Support: Baseline widely available.

Registered angle driving the haze

The glow blob reads one registered <angle>, --glow-angle, which transitions from 0deg to 180deg:

translate: calc(-50% + sin(var(--glow-angle, 0deg)) * var(--r)) 50%;
opacity: calc(.2 + .7 * sin(var(--glow-angle, 0deg)));

sin() is 0 at both ends and 1 at 90deg, so the haze is faint at noon and midnight, brightest and farthest to the side mid-swing (sunset), and moves in x in step with the sun. Two properties derive from one number, so they cannot drift apart. The honest cost: this is not compositor-driven. Style recalculation runs on the main thread each frame to resolve translate and opacity from the transitioning angle, followed by a layer update. It is one small element.

The , 0deg default in var() matters. Without @property support the variable would be invalid, the calc() would be invalid at computed-value time, and opacity would fall back to full. The default keeps day quiet. That fix was added after a reviewer saw the unguarded glow blob; it has not been re-tested in a browser without @property. Support: sin() is Baseline widely available; @property as above.

sibling-index() stagger with --i fallback

The 12 lit windows (.lit rect) fade in one by one at night and out in reverse order by day. There is a fallback with an inline --i on each rect, then an @supports upgrade:

@supports (transition-delay: calc(sibling-index() * 1ms)) {
  .lit rect { transition-delay: calc((sibling-count() - sibling-index()) * 60ms); }
  #night:checked ~ .world .lit rect { transition-delay: calc(sibling-index() * 140ms + 1.6s); }
}

The fallback is calc(var(--i) * 140ms + 1.6s), with --n: 12 standing in for sibling-count(). The 1.6s offset lets the sky darken first. A review compared the two paths and found identical computed delays, 1.74s to 3.28s, for the night direction. The @supports block makes the upgrade explicit instead of relying on a parse-time drop. Support: sibling-index() and sibling-count() are Baseline newly available (2026-08-18); the --i path is the fallback.

Masked-disc crescent moon

The moon is a full disc with a circular hole cut by a mask:

mask: radial-gradient(circle at 70% 36%, transparent 0 52%, #000 53%); /* crescent bite */

The transparent circle, offset up and right, bites the disc into a crescent. No second element or clip-path is needed, and the shape stays a single background: var(--moon). Support: mask is Baseline widely available (2023-12-07).

--u stage unit and bottom-anchored SVG crop

One custom property turns the SVG's 160-unit viewBox into pixels:

--u: max(100vw / 160, 100dvh / 200);    /* one SVG viewBox unit in px; portrait crops to the town centre */

.land is calc(var(--u) * 160) wide, centred, and pinned to the bottom, and its SVG uses preserveAspectRatio="xMidYMax slice". Horizon height (--hz), orbit radius (--r), and body size (--s) are all derived from --u, so sky objects stay aligned with the landscape at any size. In portrait the town is cropped from the sides around its centre instead of shrinking.

Two-tone focus ring

The scene behind the toggle changes from light green to near-black, so one outline colour cannot work:

#night:focus-visible ~ .toggle {
  outline: 3px solid #fff;
  outline-offset: 3px;
  box-shadow: 0 0 0 3px #14121f;
}

A white outer band plus a dark inner band means one of them always contrasts. The colours are fixed, not tokens, because the backdrop follows the scene state, not the OS scheme.

light-dark() chrome and reduced motion

The toggle pill uses --c-ink, --c-paper, and --c-accent, which the token build emits as light-dark(...) (see packages/build/src/tokens.js), so the UI follows the OS scheme while the scene itself does not. Support: light-dark() is Baseline newly available (2024-05-13). Under prefers-reduced-motion: reduce, every transition in .world and the thumb gets zero duration and delay, so the toggle still works but the scene switches instantly.

Performance notes

Measured by pnpm build:

rawgzipbrotlibudgetok
9,441 B3,062 B2,597 B14,336 Byes

No exceptions are declared. The build found no script, font, img, or raster url().

One earlier review measured, in headless software rendering (indicative only, not a real-device number): a clean console and a maximum requestAnimationFrame interval of 16.8 ms.

Not measured: Lighthouse score, CLS, dropped-frame percentage, promoted layer count, and behaviour in Safari or Firefox.

What costs the most, from reading the CSS rather than profiling: the nine-colour crossfade restyles all descendants and repaints the sky gradient and SVG fills every frame for 3.6s, and the haze recomputes translate and opacity on the main thread. Both are main-thread work in a swing that happens only on user action, never idle. The orbit rotate and the window opacity fades are the cheap parts.

Accessibility

Reuse

<!doctype html>
<html lang="en">
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<meta name="color-scheme" content="light dark">
<title>Day / Night Hillside</title>
<!--css-->
<main class="stage" aria-label="A hillside town that swings from day to night when you toggle the switch">
<input type="checkbox" id="night">
<div class="world" aria-hidden="true">
<div class="stars"></div>
<div class="haze"></div>
<div class="orbit"><i class="sun"></i><i class="moon"></i></div>
<svg class="land" viewBox="0 0 160 90" preserveAspectRatio="xMidYMax slice">
<path class="far" d="M0 90V52Q20 43 40 50T80 46T120 51T160 45V90Z"/>
<path class="mid" d="M0 90V60Q30 50 60 56T110 52T160 58V90ZM40 56q-2.5-9 0-17q2.5 8 0 17zM148 58q-2.5-9 0-17q2.5 8 0 17z"/>
<path class="wall" d="M14 66v-7h10v7zM30 70v-8h12v8zM50 64v-7h9v7zM66 72v-8h11v8zM100 70v-8h12v8zM124 65v-7h10v7zM84 66v-16h6v16z"/>
<path class="roof" d="M13 59l6-4l6 4zM29 62l7-4l7 4zM49 57l5.5-4l5.5 4zM65 64l6.5-4l6.5 4zM99 62l7-4l7 4zM123 58l6-4l6 4zM83 50l4-7l4 7z"/>
<path class="pane" d="M16 61.5h2v2.5h-2zM20 61.5h2v2.5h-2zM32 64.5h2v2.5h-2zM38 64.5h2v2.5h-2zM52 59.5h2v2.5h-2zM55 59.5h2v2.5h-2zM68 66.5h2v2.5h-2zM73 66.5h2v2.5h-2zM102 64.5h2v2.5h-2zM108 64.5h2v2.5h-2zM126 60.5h2v2.5h-2zM130 60.5h2v2.5h-2zM86 53h2v4h-2z"/>
<g class="lit"><rect x="68" y="66.5" width="2" height="2.5" style="--i:1"/><rect x="73" y="66.5" width="2" height="2.5" style="--i:2"/><rect x="32" y="64.5" width="2" height="2.5" style="--i:3"/><rect x="38" y="64.5" width="2" height="2.5" style="--i:4"/><rect x="102" y="64.5" width="2" height="2.5" style="--i:5"/><rect x="108" y="64.5" width="2" height="2.5" style="--i:6"/><rect x="16" y="61.5" width="2" height="2.5" style="--i:7"/><rect x="20" y="61.5" width="2" height="2.5" style="--i:8"/><rect x="126" y="60.5" width="2" height="2.5" style="--i:9"/><rect x="130" y="60.5" width="2" height="2.5" style="--i:10"/><rect x="52" y="59.5" width="2" height="2.5" style="--i:11"/><rect x="55" y="59.5" width="2" height="2.5" style="--i:12"/></g>
<path class="near" d="M0 90V78Q40 72 80 76T160 74V90ZM145 68a7 6 0 1 0 14 0a7 6 0 1 0-14 0zM151.2 68v12h1.6v-12zM139.6 66h.8v14h-.8zM138.2 64h3.6v2.4h-3.6z"/>
<path class="trail" d="M28 90Q48 84 40 76.5"/>
</svg>
</div>
<label class="toggle" for="night">Night<span class="track"><span class="thumb"></span></span></label>
</main>
@import "lib/stage";

/* Phase palette (day -> night). Source of truth: tokens.json "phase" group.
   The build only emits color/duration/easing/angle today, so registrations and
   values live here until a `phase` emitter exists. Registered so one state
   switch crossfades the whole scene; unregistered browsers snap. */
@property --c-sky-top  { syntax: "<color>"; inherits: true; initial-value: #5aa9e6; }
@property --c-sky-low  { syntax: "<color>"; inherits: true; initial-value: #ffd9a0; }
@property --c-glow     { syntax: "<color>"; inherits: true; initial-value: #fff1c9; }
@property --c-far-hill { syntax: "<color>"; inherits: true; initial-value: #8fb8a0; }
@property --c-mid-hill { syntax: "<color>"; inherits: true; initial-value: #5f9a7a; }
@property --c-near-hill{ syntax: "<color>"; inherits: true; initial-value: #3f7a5c; }
@property --c-wall     { syntax: "<color>"; inherits: true; initial-value: #f2e3c8; }
@property --c-roof     { syntax: "<color>"; inherits: true; initial-value: #c4553a; }
@property --c-window   { syntax: "<color>"; inherits: true; initial-value: #5f87a8; }
@property --glow-angle { syntax: "<angle>"; inherits: false; initial-value: 0deg; }

@layer scene {
  .stage {
    --u: max(100vw / 160, 100dvh / 200);    /* one SVG viewBox unit in px; portrait crops to the town centre */
    --hz: calc(var(--u) * 40);              /* horizon height above the stage bottom */
    --r: max(10vmin, min(44vw, 100dvh - var(--u) * 54)); /* orbit radius */
    --s: max(2.5rem, var(--u) * 9);         /* sun / moon diameter */
    /* fixed celestial colours (intentionally not tokens): the same in both phases and both OS schemes */
    --sun: #ffb23e;
    --moon: #f4f1ff;
  }

  /* scene state: the checkbox precedes .world, so a sibling combinator is enough (no :has() needed) */
  #night { position: absolute; inline-size: 1px; block-size: 1px; opacity: 0; pointer-events: none; }

  .world {
    position: absolute;
    inset: 0;
    --c-sky-top: #5aa9e6; --c-sky-low: #ffd9a0; --c-glow: #fff1c9;
    --c-far-hill: #8fb8a0; --c-mid-hill: #5f9a7a; --c-near-hill: #3f7a5c;
    --c-wall: #f2e3c8; --c-roof: #c4553a; --c-window: #5f87a8;
    /* paint-ok: these are main-thread transitions. Each frame the inheriting --c-* props restyle every
       descendant, repaint the SVG fills and the full-size sky gradient. Flat fills, no blur; the sky is the
       only large repaint. */
    transition-property: --c-sky-top, --c-sky-low, --c-glow, --c-far-hill, --c-mid-hill, --c-near-hill, --c-wall, --c-roof, --c-window;
    transition-duration: var(--t-swing);
    transition-timing-function: var(--ease-out); /* same curve as the orbit so colour tracks the sun */
    background: linear-gradient(var(--c-sky-top), var(--c-sky-low) 62%);
  }
  #night:checked ~ .world {
    --c-sky-top: #0e1230; --c-sky-low: #3a2a5c; --c-glow: #6b5aa8;
    --c-far-hill: #1d2748; --c-mid-hill: #131a36; --c-near-hill: #0b1026;
    --c-wall: #4a4466; --c-roof: #5a2f3f; --c-window: #2a2748;
  }

  .stars {
    position: absolute;
    inset: 0 0 45%;
    opacity: 0;
    background:
      radial-gradient(2.5px 2.5px at 8% 20%, var(--moon), transparent),
      radial-gradient(1.5px 1.5px at 17% 52%, var(--moon), transparent),
      radial-gradient(2.5px 2.5px at 28% 12%, var(--moon), transparent),
      radial-gradient(1.5px 1.5px at 38% 38%, var(--moon), transparent),
      radial-gradient(2.5px 2.5px at 52% 22%, var(--moon), transparent),
      radial-gradient(1.5px 1.5px at 63% 8%, var(--moon), transparent),
      radial-gradient(2.5px 2.5px at 72% 44%, var(--moon), transparent),
      radial-gradient(1.5px 1.5px at 81% 16%, var(--moon), transparent),
      radial-gradient(2.5px 2.5px at 90% 34%, var(--moon), transparent),
      radial-gradient(1.5px 1.5px at 95% 6%, var(--moon), transparent);
    transition: opacity var(--t-base);
  }
  #night:checked ~ .world .stars {
    opacity: 1;
    transition: opacity calc(var(--t-slow) * 2) 1.4s; /* appear after the sky darkens */
  }

  /* one pivot on the horizon: sun above, moon below; rotating 180deg swaps them */
  .orbit {
    position: absolute;
    inset-inline-start: 50%;
    inset-block-end: var(--hz);
    inline-size: 0;
    block-size: 0;
    rotate: 0deg;
    transition: rotate var(--t-swing) var(--ease-out);
  }
  #night:checked ~ .world .orbit { rotate: 180deg; }
  .sun, .moon {
    position: absolute;
    inline-size: var(--s);
    block-size: var(--s);
    border-radius: 50%;
  }
  .sun {
    /* element is 1.8x the disc; the disc is the inner 55%, the rest is a soft gradient halo */
    inline-size: calc(var(--s) * 1.8);
    block-size: calc(var(--s) * 1.8);
    background: radial-gradient(closest-side, var(--sun) 0 55%, color-mix(in srgb, var(--sun) 40%, transparent) 58%, transparent);
    translate: -50% calc(-50% - var(--r));
  }
  .moon {
    background: var(--moon);
    mask: radial-gradient(circle at 70% 36%, transparent 0 52%, #000 53%); /* crescent bite */
    translate: -50% calc(-50% + var(--r));
  }

  /* golden-hour glow: follows the sun's x and peaks mid-swing, both derived from one angle */
  .haze {
    position: absolute;
    inset-inline-start: 50%;
    inset-block-end: var(--hz);
    inline-size: calc(var(--r) * 1.6);
    aspect-ratio: 1;
    /* NOT compositor-driven: translate and opacity are recomputed on the main thread every frame from the
       transitioning --glow-angle (style recalc, then a cheap layer update). Fallback 0deg keeps the day
       state quiet when @property is unsupported (it would otherwise be invalid, i.e. full opacity). */
    translate: calc(-50% + sin(var(--glow-angle, 0deg)) * var(--r)) 50%;
    opacity: calc(.2 + .7 * sin(var(--glow-angle, 0deg)));
    background: radial-gradient(closest-side, var(--c-glow), transparent);
    transition: --glow-angle var(--t-swing) var(--ease-out);
  }
  #night:checked ~ .world .haze { --glow-angle: 180deg; }

  .land {
    position: absolute;
    inset-inline-start: 50%;
    inset-block-end: 0;
    inline-size: calc(var(--u) * 160);
    aspect-ratio: 16 / 9;
    translate: -50% 0;
  }
  .far { fill: var(--c-far-hill); }
  .mid { fill: var(--c-mid-hill); }
  .wall { fill: var(--c-wall); }
  .roof { fill: var(--c-roof); }
  .pane { fill: var(--c-window); }
  .near { fill: var(--c-near-hill); }
  .trail { fill: none; stroke: var(--c-wall); stroke-width: 2; stroke-linecap: round; opacity: .45; }

  /* windows: the lit overlay fades (opacity), staggered candle-by-candle */
  .lit { --n: 12; } /* window count, for the --i fallback; the sibling-count() path needs no number */
  .lit rect {
    fill: var(--c-lamp);
    stroke: var(--c-lamp);
    stroke-width: .8;
    stroke-opacity: .3;
    opacity: 0;
    transition: opacity var(--t-slow);
    transition-delay: calc((var(--n) - var(--i)) * 60ms);
  }
  #night:checked ~ .world .lit rect {
    opacity: 1;
    transition-delay: calc(var(--i) * 140ms + 1.6s);
  }
  @supports (transition-delay: calc(sibling-index() * 1ms)) {
    .lit rect { transition-delay: calc((sibling-count() - sibling-index()) * 60ms); }
    #night:checked ~ .world .lit rect { transition-delay: calc(sibling-index() * 140ms + 1.6s); }
  }

  /* UI chrome */
  .toggle {
    position: absolute;
    inset-inline-start: 50%;
    inset-block-end: max(1rem, env(safe-area-inset-bottom));
    translate: -50% 0;
    display: flex;
    align-items: center;
    gap: .75rem;
    min-block-size: 44px;
    padding-inline: 1.1rem .6rem;
    border: 2px solid var(--c-ink);
    border-radius: 99px;
    background: var(--c-paper);
    color: var(--c-ink);
    font-weight: 600;
    cursor: pointer;
  }
  .track { display: block; inline-size: 2.75rem; block-size: 1.5rem; padding: .2rem; border-radius: 99px; background: color-mix(in oklab, var(--c-ink) 18%, var(--c-paper)); }
  .thumb {
    display: block;
    inline-size: 1.1rem;
    block-size: 1.1rem;
    border-radius: 50%;
    background: var(--sun);
    transition: translate var(--t-base) var(--ease-out), background-color var(--t-base); /* paint-ok: 1.1rem dot */
  }
  #night:checked ~ .toggle .thumb { translate: 1.25rem 0; background: var(--c-accent); }
  /* Two-tone ring: dark inner band + white outer band, so one band always contrasts with the scene behind
     (near-hill is light green by day, near-black by night). Fixed colours on purpose: the backdrop follows
     the scene state, not the OS colour scheme. */
  #night:focus-visible ~ .toggle {
    outline: 3px solid #fff;
    outline-offset: 3px;
    box-shadow: 0 0 0 3px #14121f;
  }

  @media (prefers-reduced-motion: reduce) {
    .world, .world *, .thumb { transition-duration: 0s !important; transition-delay: 0s !important; }
  }
}

What ships: tokens and lib snippets inlined, lowered for the browser targets (minified in the real page).

@property --c-ink {
  syntax: "<color>";
  inherits: true;
  initial-value: #14121f;
}

@property --c-paper {
  syntax: "<color>";
  inherits: true;
  initial-value: #f4f1ff;
}

@property --c-accent {
  syntax: "<color>";
  inherits: true;
  initial-value: #6d4aff;
}

@property --c-car-wall {
  syntax: "<color>";
  inherits: true;
  initial-value: #e9e6dc;
}

@property --c-seat {
  syntax: "<color>";
  inherits: true;
  initial-value: #c9c4b4;
}

@property --c-line {
  syntax: "<color>";
  inherits: true;
  initial-value: #ff9500;
}

@property --c-tunnel {
  syntax: "<color>";
  inherits: true;
  initial-value: #05050a;
}

@property --c-lamp {
  syntax: "<color>";
  inherits: true;
  initial-value: #ffe9b0;
}

@property --c-platform {
  syntax: "<color>";
  inherits: true;
  initial-value: #dfe9ff;
}

@property --a-spin {
  syntax: "<angle>";
  inherits: false;
  initial-value: 0deg;
}

@layer tokens {
  :root {
    color-scheme: light dark;
    --c-ink: light-dark(#14121f, #f4f1ff);
    --c-paper: light-dark(#f4f1ff, #0b0a14);
    --c-accent: light-dark(#6d4aff, #9d85ff);
    --c-car-wall: light-dark(#e9e6dc, #1a1826);
    --c-seat: light-dark(#c9c4b4, #2a2638);
    --c-line: light-dark(#ff9500, #ff9f1a);
    --c-tunnel: #05050a;
    --c-lamp: #ffe9b0;
    --c-platform: light-dark(#dfe9ff, #9fb4e8);
    --t-fast: .15s;
    --t-base: .4s;
    --t-slow: 1.2s;
    --t-drift: 24s;
    --t-rush: 1.6s;
    --t-swing: 3.6s;
    --ease-out: cubic-bezier(.22, 1, .36, 1);
    --ease-spring: linear(0, .22 2.1%, .73 6.5%, 1.04 10.7%, 1.09 13%, 1.02 20%, .99 27%, 1);
  }
}

@layer reset {
  *, :before, :after {
    box-sizing: border-box;
    margin: 0;
  }

  html {
    block-size: 100%;
  }
}

@layer base {
  body {
    background: var(--c-paper);
    min-block-size: 100dvh;
    color: var(--c-ink);
    font: 1rem / 1.5 system-ui, sans-serif;
    overflow: clip;
  }

  .stage {
    contain: strict;
    position: fixed;
    inset: 0;
    overflow: clip;
  }
}

@layer scene {
  .stage {
    --u: max(100vw / 160, 100dvh / 200);
    --hz: calc(var(--u) * 40);
    --r: max(10vmin, min(44vw, 100dvh - var(--u) * 54));
    --s: max(2.5rem, var(--u) * 9);
    --sun: #ffb23e;
    --moon: #f4f1ff;
  }

  #night {
    opacity: 0;
    pointer-events: none;
    block-size: 1px;
    inline-size: 1px;
    position: absolute;
  }

  .world {
    --c-sky-top: #5aa9e6;
    --c-sky-low: #ffd9a0;
    --c-glow: #fff1c9;
    --c-far-hill: #8fb8a0;
    --c-mid-hill: #5f9a7a;
    --c-near-hill: #3f7a5c;
    --c-wall: #f2e3c8;
    --c-roof: #c4553a;
    --c-window: #5f87a8;
    transition-property: --c-sky-top, --c-sky-low, --c-glow, --c-far-hill, --c-mid-hill, --c-near-hill, --c-wall, --c-roof, --c-window;
    transition-duration: var(--t-swing);
    transition-timing-function: var(--ease-out);
    background: linear-gradient(var(--c-sky-top), var(--c-sky-low) 62%);
    position: absolute;
    inset: 0;
  }

  #night:checked ~ .world {
    --c-sky-top: #0e1230;
    --c-sky-low: #3a2a5c;
    --c-glow: #6b5aa8;
    --c-far-hill: #1d2748;
    --c-mid-hill: #131a36;
    --c-near-hill: #0b1026;
    --c-wall: #4a4466;
    --c-roof: #5a2f3f;
    --c-window: #2a2748;
  }

  .stars {
    opacity: 0;
    background: radial-gradient(2.5px 2.5px at 8% 20%, var(--moon), transparent),
      radial-gradient(1.5px 1.5px at 17% 52%, var(--moon), transparent),
      radial-gradient(2.5px 2.5px at 28% 12%, var(--moon), transparent),
      radial-gradient(1.5px 1.5px at 38% 38%, var(--moon), transparent),
      radial-gradient(2.5px 2.5px at 52% 22%, var(--moon), transparent),
      radial-gradient(1.5px 1.5px at 63% 8%, var(--moon), transparent),
      radial-gradient(2.5px 2.5px at 72% 44%, var(--moon), transparent),
      radial-gradient(1.5px 1.5px at 81% 16%, var(--moon), transparent),
      radial-gradient(2.5px 2.5px at 90% 34%, var(--moon), transparent),
      radial-gradient(1.5px 1.5px at 95% 6%, var(--moon), transparent);
    transition: opacity var(--t-base);
    position: absolute;
    inset: 0 0 45%;
  }

  #night:checked ~ .world .stars {
    opacity: 1;
    transition: opacity calc(var(--t-slow) * 2) 1.4s;
  }

  .orbit {
    block-size: 0;
    inline-size: 0;
    transition: rotate var(--t-swing) var(--ease-out);
    position: absolute;
    inset-block-end: var(--hz);
    inset-inline-start: 50%;
    rotate: 0deg;
  }

  #night:checked ~ .world .orbit {
    rotate: 180deg;
  }

  .sun, .moon {
    inline-size: var(--s);
    block-size: var(--s);
    border-radius: 50%;
    position: absolute;
  }

  .sun {
    inline-size: calc(var(--s) * 1.8);
    block-size: calc(var(--s) * 1.8);
    background: radial-gradient(closest-side, var(--sun) 0 55%, color-mix(in srgb, var(--sun) 40%, transparent) 58%, transparent);
    translate: -50% calc(-50% - var(--r));
  }

  .moon {
    background: var(--moon);
    translate: -50% calc(-50% + var(--r));
    mask: radial-gradient(circle at 70% 36%, #0000 0 52%, #000 53%);
  }

  .haze {
    inline-size: calc(var(--r) * 1.6);
    aspect-ratio: 1;
    translate: calc(-50% + sin(var(--glow-angle, 0deg)) * var(--r)) 50%;
    opacity: calc(.2 + .7 * sin(var(--glow-angle, 0deg)));
    background: radial-gradient(closest-side, var(--c-glow), transparent);
    transition: --glow-angle var(--t-swing) var(--ease-out);
    position: absolute;
    inset-block-end: var(--hz);
    inset-inline-start: 50%;
  }

  #night:checked ~ .world .haze {
    --glow-angle: 180deg;
  }

  .land {
    inline-size: calc(var(--u) * 160);
    aspect-ratio: 16 / 9;
    position: absolute;
    inset-block-end: 0;
    inset-inline-start: 50%;
    translate: -50%;
  }

  .far {
    fill: var(--c-far-hill);
  }

  .mid {
    fill: var(--c-mid-hill);
  }

  .wall {
    fill: var(--c-wall);
  }

  .roof {
    fill: var(--c-roof);
  }

  .pane {
    fill: var(--c-window);
  }

  .near {
    fill: var(--c-near-hill);
  }

  .trail {
    fill: none;
    stroke: var(--c-wall);
    stroke-width: 2px;
    stroke-linecap: round;
    opacity: .45;
  }

  .lit {
    --n: 12;
  }

  .lit rect {
    fill: var(--c-lamp);
    stroke: var(--c-lamp);
    stroke-width: .8px;
    stroke-opacity: .3;
    opacity: 0;
    transition: opacity var(--t-slow);
    transition-delay: calc((var(--n) - var(--i)) * 60ms);
  }

  #night:checked ~ .world .lit rect {
    opacity: 1;
    transition-delay: calc(var(--i) * .14s + 1.6s);
  }

  @supports (transition-delay: calc(sibling-index() * 1ms)) {
    .lit rect {
      transition-delay: calc((sibling-count() - sibling-index()) * 60ms);
    }

    #night:checked ~ .world .lit rect {
      transition-delay: calc(sibling-index() * .14s + 1.6s);
    }
  }

  .toggle {
    border: 2px solid var(--c-ink);
    background: var(--c-paper);
    min-block-size: 44px;
    color: var(--c-ink);
    cursor: pointer;
    border-radius: 99px;
    align-items: center;
    gap: .75rem;
    padding-inline: 1.1rem .6rem;
    font-weight: 600;
    display: flex;
    position: absolute;
    inset-block-end: max(1rem, env(safe-area-inset-bottom));
    inset-inline-start: 50%;
    translate: -50%;
  }

  .track {
    background: color-mix(in oklab, var(--c-ink) 18%, var(--c-paper));
    border-radius: 99px;
    block-size: 1.5rem;
    inline-size: 2.75rem;
    padding: .2rem;
    display: block;
  }

  .thumb {
    background: var(--sun);
    block-size: 1.1rem;
    inline-size: 1.1rem;
    transition: translate var(--t-base) var(--ease-out), background-color var(--t-base);
    border-radius: 50%;
    display: block;
  }

  #night:checked ~ .toggle .thumb {
    background: var(--c-accent);
    translate: 1.25rem;
  }

  #night:focus-visible ~ .toggle {
    outline-offset: 3px;
    outline: 3px solid #fff;
    box-shadow: 0 0 0 3px #14121f;
  }

  @media (prefers-reduced-motion: reduce) {
    .world, .world *, .thumb {
      transition-duration: 0s !important;
      transition-delay: 0s !important;
    }
  }
}

@layer utilities;

@property --c-sky-top {
  syntax: "<color>";
  inherits: true;
  initial-value: #5aa9e6;
}

@property --c-sky-low {
  syntax: "<color>";
  inherits: true;
  initial-value: #ffd9a0;
}

@property --c-glow {
  syntax: "<color>";
  inherits: true;
  initial-value: #fff1c9;
}

@property --c-far-hill {
  syntax: "<color>";
  inherits: true;
  initial-value: #8fb8a0;
}

@property --c-mid-hill {
  syntax: "<color>";
  inherits: true;
  initial-value: #5f9a7a;
}

@property --c-near-hill {
  syntax: "<color>";
  inherits: true;
  initial-value: #3f7a5c;
}

@property --c-wall {
  syntax: "<color>";
  inherits: true;
  initial-value: #f2e3c8;
}

@property --c-roof {
  syntax: "<color>";
  inherits: true;
  initial-value: #c4553a;
}

@property --c-window {
  syntax: "<color>";
  inherits: true;
  initial-value: #5f87a8;
}

@property --glow-angle {
  syntax: "<angle>";
  inherits: false;
  initial-value: 0deg;
}