heartwood every commit a ring

first light

da13d0fc by Isaac Bythewood · 1 month ago

first light

static almanac that breathes with the season and the hour.
markdown knowledge store, daylight cycle, moon phase math,
word-by-word reveal. no frameworks, no apis, no build steps.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
added .gitignore
@@ -0,0 +1,2 @@.env__pycache__/
added CLAUDE.md
@@ -0,0 +1,48 @@# Dark Furrow## What is this project?Dark Furrow is a seasonal almanac for the modern web. It surfaces forgottenrhythms that humans used to live by: what's growing right now, what's readyto harvest, what the sky is doing, what our ancestors called this week.The project is an art piece as much as it is a tool. It is a single livingpage that breathes with the seasons.## Design philosophy- Text-forward, minimal, dark aesthetic. Like reading by firelight.- Clean, simple text-based UI. No flashy components or frameworks.- No external API dependencies. All data is encoded by hand or calculated  with simple math (moon phases, sunrise/sunset, daylight changes).- No feeds, no accounts, no engagement patterns.- The page should feel quiet, warm, and cozy.## Technical notes- No frameworks unless absolutely necessary. Prefer plain HTML, CSS, and  vanilla JavaScript.- No build steps if possible. Just files that work.- The site will live at darkfurrow.com.- Target planting zone is 7a (North Carolina) to start, but the structure  should allow for expansion to other zones later.## Content the page should surface- What's in season to plant and harvest right now- Seasonal food and simple cooking ideas (no fish or seafood)- Moon phase, sunrise/sunset, daylight length changes- Old folk names for storms, stars, and time periods- Practical wisdom that used to be passed down but no longer is## Voice and tone- Poetic but not pretentious- Warm but not sentimental- The writing should feel like it belongs in an old book you found  in a quiet shop## Formatting preferences- Avoid em dashes and en dashes in all writing- Keep things lowercase where it feels right
added Dockerfile
@@ -0,0 +1,11 @@FROM python:3.13-alpineRUN addgroup -S -g 1000 app && \    adduser -S -h /app -s /sbin/nologin -u 1000 -G app app && \    chown -R app:app /appWORKDIR /appCOPY site/ /app/site/USER app
added Makefile
@@ -0,0 +1,8 @@.PHONY: run pushrun:	python3 -m http.server 8000 --directory sitepush:	git push origin master	git push server master
added docker-compose.yml
@@ -0,0 +1,9 @@services:  web:    container_name: darkfurrow.com    build: .    env_file: .env    ports:      - "127.0.0.1:${PORT}:${PORT}"    command: python3 -m http.server ${PORT} --directory site    restart: unless-stopped
added samplefiles/env.sample
@@ -0,0 +1 @@PORT=8500
added samplefiles/post-receive.sample
@@ -0,0 +1,14 @@#!/bin/shwhile read oldrev newrev ref; do  if [ "$ref" = "refs/heads/master" ]; then    unset GIT_DIR    START_TIME=$(date +%s)    cd /srv/docker/darkfurrow.com    git pull    docker compose up --build --detach    docker system prune --force    END_TIME=$(date +%s)    echo "Total build time: $((END_TIME - START_TIME))s"  fidone
added site/almanac.js
@@ -0,0 +1,288 @@// almanac.js//// the engine beneath the soil.// reads the clock and the calendar, fetches what is relevant,// and renders the page that belongs to this moment.(function () {  'use strict';  var LAT = 35.78; // north carolina, zone 7a  // --- time and season ---  var SEASONS = [    { name: 'winter',       start: [1, 1],   end: [2, 28],  label: 'winter',       note: 'the ground is still. the light is short.\nrest is not emptiness, it is preparation.' },    { name: 'early-spring', start: [3, 1],   end: [4, 15],  label: 'early spring',  note: 'the soil warms. the light returns longer each day.\nwhat was sleeping is not sleeping anymore.' },    { name: 'late-spring',  start: [4, 16],  end: [5, 31],  label: 'late spring',   note: 'the frost is gone or nearly gone.\neverything is rushing now. the green is loud.' },    { name: 'early-summer', start: [6, 1],   end: [6, 30],  label: 'early summer',  note: 'the days are longest. the heat is building.\nthe garden is a job now, not a hope.' },    { name: 'midsummer',    start: [7, 1],   end: [8, 31],  label: 'midsummer',     note: 'the full weight of summer.\neverything is ripe or ripening or done.' },    { name: 'early-fall',   start: [9, 1],   end: [10, 31], label: 'early fall',    note: 'the light is leaving but slowly.\nmornings are cool again. the garden exhales.' },    { name: 'late-fall',    start: [11, 1],  end: [11, 30],  label: 'late fall',     note: 'the trees are bare or nearly.\nthe first frost has come or is coming tonight.' },    { name: 'winter',       start: [12, 1],  end: [12, 31], label: 'winter',        note: 'the ground is still. the light is short.\nrest is not emptiness, it is preparation.' }  ];  var TIMES = [    { name: 'night',     start: 0,  end: 5  },    { name: 'dawn',      start: 5,  end: 8  },    { name: 'morning',   start: 8,  end: 12 },    { name: 'afternoon', start: 12, end: 17 },    { name: 'evening',   start: 17, end: 21 },    { name: 'night',     start: 21, end: 24 }  ];  function getSeason(date) {    var m = date.getMonth() + 1;    var d = date.getDate();    for (var i = 0; i < SEASONS.length; i++) {      var s = SEASONS[i];      var afterStart = m > s.start[0] || (m === s.start[0] && d >= s.start[1]);      var beforeEnd = m < s.end[0] || (m === s.end[0] && d <= s.end[1]);      if (afterStart && beforeEnd) return s;    }    return SEASONS[0];  }  function getTimeOfDay(date) {    var h = date.getHours();    for (var i = 0; i < TIMES.length; i++) {      if (h >= TIMES[i].start && h < TIMES[i].end) return TIMES[i].name;    }    return 'night';  }  // --- daylight cycle ---  // shifts the palette based on time of day  var CYCLES = {    night:     { bg: '#080706', text: '#9a8e80', accent: '#4a6340', heading: '#7a6a58', glow: 'rgba(60,50,35,0.08)' },    dawn:      { bg: '#0f0c09', text: '#c4b5a0', accent: '#7a9a62', heading: '#c0885a', glow: 'rgba(180,120,60,0.06)' },    morning:   { bg: '#0e0c0a', text: '#d4c8b8', accent: '#7a9a62', heading: '#b07a50', glow: 'rgba(140,100,50,0.04)' },    afternoon: { bg: '#0d0b09', text: '#d0c2b0', accent: '#6a8a55', heading: '#a87048', glow: 'rgba(160,100,40,0.05)' },    evening:   { bg: '#0b0908', text: '#c8b8a5', accent: '#5a7d4a', heading: '#b87840', glow: 'rgba(180,100,40,0.08)' }  };  function applyDaylightCycle(time) {    var c = CYCLES[time] || CYCLES.morning;    var r = document.documentElement;    r.style.setProperty('--earth', c.bg);    r.style.setProperty('--bone', c.text);    r.style.setProperty('--sprout', c.accent);    r.style.setProperty('--ember', c.heading);    r.style.setProperty('--glow', c.glow);    document.body.setAttribute('data-time', time);  }  // --- sky calculations ---  function moonPhase(date) {    var known = new Date(Date.UTC(2000, 0, 6, 18, 14, 0));    var synodic = 29.53058867;    var diff = (date.getTime() - known.getTime()) / 86400000;    return ((diff % synodic) + synodic) % synodic;  }  function moonName(phase) {    if (phase < 1.85) return 'new moon';    if (phase < 7.38) return 'waxing crescent';    if (phase < 9.23) return 'first quarter';    if (phase < 14.77) return 'waxing gibbous';    if (phase < 16.61) return 'full moon';    if (phase < 22.15) return 'waning gibbous';    if (phase < 23.99) return 'last quarter';    if (phase < 27.68) return 'waning crescent';    return 'new moon';  }  function moonIllumination(phase) {    return (1 - Math.cos(2 * Math.PI * phase / 29.53058867)) / 2;  }  function daylight(date, lat) {    var doy = Math.floor((date - new Date(date.getFullYear(), 0, 0)) / 86400000);    var decl = 23.45 * Math.sin((2 * Math.PI / 365) * (doy - 81));    var cosH = -Math.tan(lat * Math.PI / 180) * Math.tan(decl * Math.PI / 180);    cosH = Math.max(-1, Math.min(1, cosH));    return (2 * Math.acos(cosH) * 180 / Math.PI) / 15;  }  function formatHM(hours) {    var h = Math.floor(hours);    var m = Math.round((hours - h) * 60);    return h + 'h ' + m + 'm';  }  function skyText(now, time) {    var phase = moonPhase(now);    var name = moonName(phase);    var illum = Math.round(moonIllumination(phase) * 100);    var hours = daylight(now, LAT);    var yesterday = new Date(now);    yesterday.setDate(yesterday.getDate() - 1);    var gained = ((hours - daylight(yesterday, LAT)) * 60).toFixed(1);    var sign = gained > 0 ? '+' : '';    var lines = [];    if (time === 'night') {      lines.push('the moon is ' + name + ', ' + illum + '% lit.');      lines.push('the world is turned away from the sun.');      lines.push(formatHM(hours) + ' of daylight today. ' + sign + gained + ' minutes from yesterday.');    } else if (time === 'dawn') {      lines.push('the sun is finding the edge of things.');      lines.push('the moon is ' + name + ', ' + illum + '% lit.');      lines.push(formatHM(hours) + ' of daylight ahead.');    } else if (time === 'evening') {      lines.push('the light is going.');      lines.push('the moon is ' + name + ', ' + illum + '% lit.');      lines.push('there were ' + formatHM(hours) + ' of daylight today. ' + sign + gained + ' minutes from yesterday.');    } else {      lines.push('the moon is ' + name + ', ' + illum + '% lit.');      lines.push(formatHM(hours) + ' of daylight today.');      lines.push(sign + gained + ' minutes from yesterday, quietly.');    }    return lines.join('\n');  }  // --- markdown parsing ---  // just enough to render what we write. no libraries.  function parseFrontmatter(text) {    var match = text.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/);    if (!match) return { meta: {}, body: text };    var meta = {};    match[1].split('\n').forEach(function (line) {      var parts = line.split(/:\s*/);      if (parts.length >= 2) meta[parts[0].trim()] = parts.slice(1).join(':').trim();    });    return { meta: meta, body: match[2].trim() };  }  function renderMarkdown(body) {    var lines = body.split('\n');    var html = '';    var inList = false;    lines.forEach(function (line) {      var trimmed = line.trim();      if (trimmed.match(/^- /)) {        if (!inList) { html += '<ul>'; inList = true; }        html += '<li>' + trimmed.slice(2) + '</li>';      } else {        if (inList) { html += '</ul>'; inList = false; }        if (trimmed === '') {          html += '<br>';        } else {          html += trimmed + '<br>';        }      }    });    if (inList) html += '</ul>';    return html;  }  // --- word reveal ---  function revealWords(root) {    var elements = root.querySelectorAll('h2, p, li');    var allWords = [];    elements.forEach(function (el) {      var nodes = [];      el.childNodes.forEach(function (node) {        if (node.nodeType === 3) {          node.textContent.split(/(\s+)/).forEach(function (w) {            if (/^\s*$/.test(w)) {              nodes.push(document.createTextNode(w));            } else {              var span = document.createElement('span');              span.className = 'word';              span.textContent = w;              nodes.push(span);              allWords.push(span);            }          });        } else {          nodes.push(node.cloneNode(true));        }      });      el.textContent = '';      nodes.forEach(function (n) { el.appendChild(n); });    });    var delay = 0;    allWords.forEach(function (span) {      span.style.animationDelay = delay + 'ms';      delay += 18;    });  }  // --- main ---  function render() {    var now = new Date();    var season = getSeason(now);    var time = getTimeOfDay(now);    // daylight cycle    applyDaylightCycle(time);    // season header    document.querySelector('.season-name').textContent = season.label;    document.querySelector('.season-note').innerHTML = season.note.replace(/\n/g, '<br>');    // sky    var skyP = document.querySelector('.sky p');    skyP.innerHTML = skyText(now, time).replace(/\n/g, '<br>');    // fetch content    fetch('/data/manifest.json')      .then(function (r) { return r.json(); })      .then(function (manifest) {        // filter to current season and time of day        var relevant = manifest.filter(function (entry) {          if (entry.season && entry.season !== season.name) return false;          if (entry.time && entry.time !== time) return false;          return true;        });        return Promise.all(relevant.map(function (entry) {          return fetch('/data/' + entry.path)            .then(function (r) { return r.text(); })            .then(function (text) { return parseFrontmatter(text); });        }));      })      .then(function (entries) {        var almanac = document.querySelector('.almanac');        almanac.innerHTML = '';        entries.forEach(function (entry) {          var div = document.createElement('div');          div.className = 'entry';          var h2 = document.createElement('h2');          h2.textContent = entry.meta.section || '';          div.appendChild(h2);          var content = document.createElement('div');          content.innerHTML = renderMarkdown(entry.body);          div.appendChild(content);          almanac.appendChild(div);        });        // reveal everything        revealWords(document.querySelector('main'));      });  }  render();})();
added site/data/kitchen/early-fall.md
@@ -0,0 +1,10 @@---season: early-fallsection: in the kitchen---- apples from the orchard, not the store- roasted squash with sage and brown butter- soups return, the pot comes back to the stove- pumpkin in things, but not everything- warm drinks in the evening, the hands want holding
added site/data/kitchen/early-spring.md
@@ -0,0 +1,9 @@---season: early-springsection: in the kitchen---- leeks and potatoes into a slow pot with thyme- the last of the stored apples, baked with butter and cinnamon- eggs from hens that are laying again after the dark months- bread with whatever is on hand, the oldest recipe
added site/data/kitchen/early-summer.md
@@ -0,0 +1,10 @@---season: early-summersection: in the kitchen---- tomatoes still warm from the vine, sliced with salt- zucchini in everything, there is always too much- cucumber water in a jar on the counter- grilled corn, butter, a little cayenne- peaches if they came in, sliced over vanilla ice cream
added site/data/kitchen/late-fall.md
@@ -0,0 +1,9 @@---season: late-fallsection: in the kitchen---- root vegetables, the ones that kept in the cellar- bone broth simmering all day, the house smells like home- dried herbs from summer, still good, crush them between your fingers- baking bread because the oven warms the room twice
added site/data/kitchen/late-spring.md
@@ -0,0 +1,10 @@---season: late-springsection: in the kitchen---- strawberries, finally, warm from the sun- asparagus while it lasts, roasted simply with oil and salt- fresh peas eaten standing in the garden, never enough make it inside- radishes sliced thin with butter on good bread- the first herbs cut and scattered over everything
added site/data/kitchen/midsummer.md
@@ -0,0 +1,10 @@---season: midsummersection: in the kitchen---- it is too hot to cook, so don't- cold soups, gazpacho, things from the fridge- watermelon with salt, the oldest summer trick- can or freeze what you cannot eat, winter will want it- eat outside if there is a breeze
added site/data/kitchen/winter.md
@@ -0,0 +1,10 @@---season: wintersection: in the kitchen---- stews that cook all afternoon, no rush now- preserved things from the shelves, last summer in a jar- potatoes in every form, the reliable companion- hot cider with cinnamon on the stove- bake something and give half away
added site/data/manifest.json
@@ -0,0 +1,29 @@[  { "path": "planting/early-spring.md", "season": "early-spring" },  { "path": "planting/early-spring-indoors.md", "season": "early-spring" },  { "path": "planting/late-spring.md", "season": "late-spring" },  { "path": "planting/early-summer.md", "season": "early-summer" },  { "path": "planting/midsummer.md", "season": "midsummer" },  { "path": "planting/early-fall.md", "season": "early-fall" },  { "path": "planting/late-fall.md", "season": "late-fall" },  { "path": "planting/winter.md", "season": "winter" },  { "path": "kitchen/early-spring.md", "season": "early-spring" },  { "path": "kitchen/late-spring.md", "season": "late-spring" },  { "path": "kitchen/early-summer.md", "season": "early-summer" },  { "path": "kitchen/midsummer.md", "season": "midsummer" },  { "path": "kitchen/early-fall.md", "season": "early-fall" },  { "path": "kitchen/late-fall.md", "season": "late-fall" },  { "path": "kitchen/winter.md", "season": "winter" },  { "path": "names/early-spring.md", "season": "early-spring" },  { "path": "names/late-spring.md", "season": "late-spring" },  { "path": "names/early-summer.md", "season": "early-summer" },  { "path": "names/midsummer.md", "season": "midsummer" },  { "path": "names/early-fall.md", "season": "early-fall" },  { "path": "names/late-fall.md", "season": "late-fall" },  { "path": "names/winter.md", "season": "winter" },  { "path": "wisdom/dawn.md", "time": "dawn" },  { "path": "wisdom/morning.md", "time": "morning" },  { "path": "wisdom/afternoon.md", "time": "afternoon" },  { "path": "wisdom/evening.md", "time": "evening" },  { "path": "wisdom/night.md", "time": "night" }]
added site/data/names/early-fall.md
@@ -0,0 +1,10 @@---season: early-fallsection: old names for this time---the harvest moon, the one that rises close to sunsetand gives extra light to work by.the corn moon. the barley moon.the equinox passes and the dark starts winning again,two minutes stolen each day now.
added site/data/names/early-spring.md
@@ -0,0 +1,10 @@---season: early-springsection: old names for this time---the worm moon is coming or just passed.some called this the crow moon, for the sound thatbreaks the silence at the end of winter.the sap moon, because the maples are running.the ground is unlocking.
added site/data/names/early-summer.md
@@ -0,0 +1,10 @@---season: early-summersection: old names for this time---the strawberry moon, the rose moon.midsummer approaches, the longest days.the anglo-saxons called june sere-month,the dry month, when the fields bake and harden.the light stays so long you forget it leaves.
added site/data/names/late-fall.md
@@ -0,0 +1,9 @@---season: late-fallsection: old names for this time---the beaver moon, when the traps were set before freeze.the frost moon. the mourning moon.the celts called this samhain, the thinning of the veil.the year is turning toward the dark half now.
added site/data/names/late-spring.md
@@ -0,0 +1,10 @@---season: late-springsection: old names for this time---the flower moon, for obvious reasons.the planting moon. the milk moon.the old english called may thrimilce,the month the cows could be milked three times a day.everything is producing.
added site/data/names/midsummer.md
@@ -0,0 +1,9 @@---season: midsummersection: old names for this time---the buck moon, the thunder moon.the dog days begin when sirius rises with the sun.the old farmers blamed the star for the heat.it is not the star. but the name stuck.
added site/data/names/winter.md
@@ -0,0 +1,10 @@---season: wintersection: old names for this time---the cold moon. the long night moon.the wolf moon in january, when they howled at the door.the anglo-saxons called december giuli, the yule month,when the sun stands still and then returns.the longest dark, and then the turn.
added site/data/planting/early-fall.md
@@ -0,0 +1,9 @@---season: early-fallsection: what to put in the ground---- garlic goes in now, it sleeps through winter and wakes strong- lettuce and greens again, the cool returns- cover crops where the beds are empty, clover or rye- plant bulbs for spring, an act of faith in the dark
added site/data/planting/early-spring-indoors.md
@@ -0,0 +1,8 @@---season: early-springsection: what to bring inside---- start tomatoes and peppers under light- herbs in small pots on a warm sill- marigolds, to walk out with the tomatoes later
added site/data/planting/early-spring.md
@@ -0,0 +1,10 @@---season: early-springsection: what to put in the ground---- peas, direct sow, while the soil is still cool- lettuce and spinach, scatter them, they forgive- onion sets, push them in like small promises- potatoes, eyes up, in trenches of loose earth- radishes, the fastest kindness a garden returns
added site/data/planting/early-summer.md
@@ -0,0 +1,10 @@---season: early-summersection: what to tend---- mulch everything, the heat is coming- water deep and less often, teach the roots to reach- pinch the suckers from the tomatoes- thin what is too close, a hard kindness- the weeds are fast now, stay ahead or make peace
added site/data/planting/late-fall.md
@@ -0,0 +1,9 @@---season: late-fallsection: putting the garden to bed---- pull the spent plants, compost what is clean- mulch the perennials deep, tuck them in- clean and oil the tools, they earned it- the ground will rest now and so should you
added site/data/planting/late-spring.md
@@ -0,0 +1,11 @@---season: late-springsection: what to put in the ground---- tomatoes go out after the last frost, finally- peppers beside them, they like the same heat- beans, pole and bush, direct into warm soil- squash and cucumbers, give them room to sprawl- corn in blocks, not rows, so the wind can do its work- basil near the tomatoes, old companions
added site/data/planting/midsummer.md
@@ -0,0 +1,10 @@---season: midsummersection: what to tend---- harvest in the morning before the heat sets in- second planting of beans if you have the space- let some herbs bolt and flower for the bees- the garden is giving now, keep up with it or it spoils- save seeds from what did well, close the circle
added site/data/planting/winter.md
@@ -0,0 +1,10 @@---season: wintersection: what grows in the quiet---- nothing to plant outside, the ground is holding still- start planning next year, draw the beds on paper- order seeds early, the good ones sell out- sharpen things, mend things, read the catalogs- the garden is not gone, it is just below
added site/data/wisdom/afternoon.md
@@ -0,0 +1,9 @@---time: afternoonsection: at this hour---do not plant now. the roots will scorch.water at the base, not the leaves, or the sunwill burn through the droplets like tiny lenses.rest if you can. the garden does not need you every hour.
added site/data/wisdom/dawn.md
@@ -0,0 +1,8 @@---time: dawnsection: at this hour---the old farmers were already up.there is dew on everything, which means the day will be fair.the birds started before you did.
added site/data/wisdom/evening.md
@@ -0,0 +1,9 @@---time: eveningsection: at this hour---water now if the soil is dry. the night gives it time to soak.check the sky for tomorrow. red in the west means fair weather.the evening is for walking the rows and noticing what changed.pull a weed or two on your way back in.
added site/data/wisdom/morning.md
@@ -0,0 +1,9 @@---time: morningsection: at this hour---the best time to transplant is early, before the sun is high.the best time to harvest herbs is after the dew driesbut before the heat draws the oils out.the morning is for doing. the afternoon is for bearing it.
added site/data/wisdom/night.md
@@ -0,0 +1,9 @@---time: nightsection: at this hour---the garden is working without you now.roots grow in the dark. the soil breathes.moths are pollinating the flowers you thought were sleeping.go to bed. tomorrow there will be something new.
added site/index.html
@@ -0,0 +1,37 @@<!doctype html><html lang="en"><head>  <meta charset="utf-8">  <meta name="viewport" content="width=device-width, initial-scale=1">  <title>dark furrow</title>  <meta name="description" content="old knowledge preserved">  <link rel="stylesheet" href="/style.css"></head><body>  <main>    <header>      <h1>dark furrow</h1>      <p class="subtitle">old knowledge preserved</p>    </header>    <section class="season">      <p class="season-name"></p>      <p class="season-note"></p>    </section>    <section class="sky">      <h2>what the sky is doing</h2>      <p></p>    </section>    <section class="almanac"></section>    <footer>      <p>zone 7a &middot; north carolina &middot; tended by hand</p>    </footer>  </main>  <script src="/almanac.js"></script></body></html>
added site/style.css
@@ -0,0 +1,279 @@:root {  --earth: #0e0c0a;  --loam: #1a1714;  --bark: #2a2420;  --ash: #a89b8e;  --bone: #d4c8b8;  --parchment: #ece3d5;  --sprout: #7a9a62;  --moss: #5a7d4a;  --ember: #b07a50;  --glow: rgba(140,100,50,0.04);}* {  margin: 0;  padding: 0;  box-sizing: border-box;}html {  font-size: 18px;}body {  background-color: var(--earth);  color: var(--bone);  font-family: Georgia, 'Times New Roman', serif;  line-height: 1.7;  -webkit-font-smoothing: antialiased;  transition: background-color 2s ease, color 2s ease;}main {  max-width: 34rem;  margin: 0 auto;  padding: 4rem 1.5rem 6rem;}header {  margin-bottom: 4rem;  border-bottom: 1px solid var(--bark);  padding-bottom: 2rem;}h1 {  font-size: 2.4rem;  font-weight: 400;  letter-spacing: 0.04em;  color: var(--parchment);  margin-bottom: 0.3rem;}.subtitle {  font-style: italic;  color: var(--ash);  font-size: 1.05rem;  animation: breathe 8s ease-in-out infinite;}.season {  margin-bottom: 3rem;  padding: 1.5rem 0;  border-bottom: 1px solid var(--bark);}.season-name {  font-size: 1.3rem;  font-weight: 500;  color: var(--sprout);  margin-bottom: 0.8rem;  letter-spacing: 0.03em;  animation: sway 6s ease-in-out infinite;  transform-origin: left center;  transition: color 2s ease;}.season-note {  color: var(--ash);  font-style: italic;  font-size: 0.95rem;  line-height: 1.8;}.sky {  margin-bottom: 3rem;  padding-bottom: 1.5rem;  border-bottom: 1px solid var(--bark);}.sky h2 {  font-size: 1.1rem;  font-weight: 500;  color: var(--ember);  margin-bottom: 0.8rem;  letter-spacing: 0.02em;  transition: color 2s ease;}.sky p {  color: var(--bone);  font-size: 0.95rem;  line-height: 1.9;}.almanac {  display: flex;  flex-direction: column;  gap: 2.8rem;}.entry h2 {  font-size: 1.1rem;  font-weight: 500;  color: var(--ember);  margin-bottom: 0.8rem;  letter-spacing: 0.02em;  transition: color 2s ease;}.entry p,.entry div {  color: var(--bone);  font-size: 0.95rem;  line-height: 1.9;}.entry ul {  list-style: none;  padding: 0;}.entry ul li {  color: var(--bone);  font-size: 0.95rem;  line-height: 1.9;  padding-left: 1.2rem;  position: relative;}.entry ul li::before {  content: '\00b7';  position: absolute;  left: 0;  color: var(--sprout);  font-weight: 500;  animation: rustle 4s ease-in-out infinite;  transition: color 2s ease;}.entry ul li:nth-child(2n)::before { animation-delay: -1.3s; }.entry ul li:nth-child(3n)::before { animation-delay: -2.7s; }.entry ul li:nth-child(5n)::before { animation-delay: -0.6s; }footer {  margin-top: 4rem;  padding-top: 2rem;  border-top: 1px solid var(--bark);}footer p {  color: var(--ash);  font-size: 0.85rem;  font-style: italic;  letter-spacing: 0.03em;}.word {  opacity: 0;  display: inline-block;  animation: word-in 0.3s ease-out forwards;}/* daylight glow */body::before {  content: '';  position: fixed;  top: 0;  left: 0;  right: 0;  bottom: 0;  pointer-events: none;  background: radial-gradient(ellipse at 50% 0%, var(--glow) 0%, transparent 70%);  transition: background 3s ease;  z-index: 9999;}/* time-of-day ambient */body[data-time="dawn"]::after {  content: '';  position: fixed;  bottom: 0;  left: 0;  right: 0;  height: 30vh;  pointer-events: none;  background: linear-gradient(to top, rgba(180,120,60,0.03) 0%, transparent 100%);  z-index: 9999;}body[data-time="evening"]::after {  content: '';  position: fixed;  top: 0;  left: 0;  right: 0;  height: 40vh;  pointer-events: none;  background: linear-gradient(to bottom, rgba(160,80,30,0.04) 0%, transparent 100%);  z-index: 9999;}body[data-time="night"]::after {  content: '';  position: fixed;  top: 0;  left: 0;  right: 0;  bottom: 0;  pointer-events: none;  background: radial-gradient(ellipse at 50% 50%, transparent 30%, rgba(0,0,0,0.15) 100%);  z-index: 9999;}::selection {  background-color: var(--moss);  color: var(--parchment);}@keyframes word-in {  from {    opacity: 0;    filter: blur(4px);  }  to {    opacity: 1;    filter: blur(0);  }}@keyframes breathe {  0%, 100% { opacity: 0.7; }  50% { opacity: 1; }}@keyframes sway {  0%, 100% { transform: translateX(0); }  30% { transform: translateX(2px); }  70% { transform: translateX(-1px); }}@keyframes rustle {  0%, 100% { transform: translateX(0) scale(1); opacity: 0.8; }  25% { transform: translateX(2px) scale(1.3); opacity: 1; }  50% { transform: translateX(-1px) scale(0.9); opacity: 0.6; }  75% { transform: translateX(1px) scale(1.1); opacity: 0.9; }}@media (prefers-reduced-motion: reduce) {  *, *::before, *::after {    animation-duration: 0.01ms !important;    animation-iteration-count: 1 !important;  }}@media (max-width: 480px) {  html {    font-size: 16px;  }  main {    padding: 3rem 1.2rem 4rem;  }  h1 {    font-size: 2rem;  }}