@@ -31,8 +31,8 @@ page that breathes with the seasons.- Production runs via `docker-compose` (Gunicorn, 2 workers) bound to `127.0.0.1:${PORT}` where `PORT` comes from `.env` (8500 on the deployed host).- Routes: `/` renders the page; `/api/content` returns the same content as JSON for the client-side auto-refresh. Both accept `?season=` and `?time=` query-string overrides for previewing other times of day or year. for the client-side auto-refresh. Both accept `?season=` for previewing other seasons. There is no `?time=` override anymore.- The site lives at darkfurrow.com.- Target planting zone is 7a (North Carolina) to start, but the structure should allow for expansion to other zones later.
@@ -48,6 +48,29 @@ page that breathes with the seasons.- The dev environment runs inside a Docker container with port 8000 mapped to the host.## Page structureThe page is one column of clearly-labeled sections so a visitor can scan to thepart they care about. The current sections, in order, are:1. **sky** - calculated sun/moon/daylight + moon-phase gardening tip + a sky lore line + a storms lore line. The italic intro is the season's mood for the current time of day (from `data/moods/`).2. **garden** - planting picks ("in the ground now"), indoor starts when the season has them, and a couple of weekly chores ("this week").3. **kitchen** - what's "in season" plus one "tonight" highlight.4. **foraging** - what the land is offering, with the closing prose line as italic lore beneath.5. **folklore** - a short paragraph each from old names, remedies, and bugs.Daily-stable seeded RNG (`seeded_random` keyed by day-of-year) picks whichitems surface per section, so content shifts day to day but is stable acrossrefreshes within a day.The time-of-day rotation that used to swap which categories appeared wasremoved. Time of day still tints the background palette (in `static/almanac.js`)but no longer hides content.## Content the page should surface- What's in season to plant and harvest right now
@@ -56,6 +79,9 @@ page that breathes with the seasons.- Old folk names for storms, stars, and time periods- Practical wisdom that used to be passed down but no longer isThe `data/wisdom/<time>.md` files are no longer rendered (they were tied to theremoved time rotation) but remain on disk in case they come back.## Voice and tone- Poetic but not pretentious
@@ -101,17 +101,6 @@ TIMES = [ {'name': 'night', 'start': 21, 'end': 24},]TIME_LABELS = ['night', 'dawn', 'morning', 'afternoon', 'evening']TIME_CONTENT = { 'night': ['sky/', 'names/', 'remedies/', 'storms/'], 'dawn': ['planting/', 'foraging/'], 'morning': ['planting/', 'chores/', 'bugs/'], 'afternoon': ['kitchen/', 'bugs/', 'chores/'], 'evening': ['kitchen/', 'remedies/', 'foraging/', 'names/', 'storms/'],}def get_time_of_day(date): h = date.hour for t in TIMES:
@@ -164,37 +153,6 @@ def parse_list_items(body): return {'bullets': bullets, 'prose': prose}def highlight_text(text): fragments = re.split(r'(?<=\.)\s+', text) result = [] for frag in fragments: trimmed = frag.strip() if not trimmed: continue plain = re.sub(r'</?strong>', '', trimmed) words = plain.split() if len(words) <= 4: result.append(f'<strong>{trimmed}</strong>') continue if '<strong>' in trimmed: result.append(trimmed) continue comma = trimmed.find(',') if 0 < comma < 30: result.append(f'<strong>{trimmed[:comma]}</strong>{trimmed[comma:]}') continue first = words[0].lower() count = 3 if first in ('the', 'a', 'an', 'if', 'when', 'it', 'and', 'or', 'but', 'do', 'in') else 2 count = min(count, len(words)) result.append(f'<strong>{" ".join(words[:count])}</strong> {" ".join(words[count:])}') return ' '.join(result)# --- sky calculations ---def moon_phase(date):
@@ -277,39 +235,22 @@ def written_date(date): return f'{time}, the {ORDINALS[date.day]} of {MONTHS[date.month - 1]}'def sky_text(now, time):def sky_data_lines(now): """three short lines: moon, sun, daylight. shown in the sky section.""" from datetime import timedelta phase = moon_phase(now) name = moon_name(phase) illum = round(moon_illumination(phase) * 100) hours = daylight_hours(now) from datetime import timedelta yesterday = now - timedelta(days=1) gained = (hours - daylight_hours(yesterday)) * 60 gained = (hours - daylight_hours(now - timedelta(days=1))) * 60 sign = '+' if gained > 0 else '' sunrise = 12 - hours / 2 sunset = 12 + hours / 2 bold_name = f'<strong>{name}</strong>' lines = [] if time == 'night': lines.append(f'the moon is {bold_name}, {illum}% lit.') lines.append('the world is turned away from the sun.') lines.append(f'<strong>{format_hm(hours)}</strong> of daylight today. {sign}{gained:.1f} minutes from yesterday.') elif time == 'dawn': lines.append(f'the sun rises around <strong>{format_clock(sunrise)}</strong>.') lines.append(f'the moon is {bold_name}, {illum}% lit.') lines.append(f'<strong>{format_hm(hours)}</strong> of daylight ahead. it sets around {format_clock(sunset)}.') elif time == 'evening': lines.append(f'the sun set around <strong>{format_clock(sunset)}</strong>.') lines.append(f'the moon is {bold_name}, {illum}% lit.') lines.append(f'there were <strong>{format_hm(hours)}</strong> of daylight today. {sign}{gained:.1f} minutes from yesterday.') else: lines.append(f'the moon is {bold_name}, {illum}% lit.') lines.append(f'the sun rose around <strong>{format_clock(sunrise)}</strong> and sets around <strong>{format_clock(sunset)}</strong>.') lines.append(f'<strong>{format_hm(hours)}</strong> of daylight today. {sign}{gained:.1f} minutes from yesterday.') return '<br>'.join(lines) return [ f'<strong>{name}</strong>, {illum}% lit', f'sunrise <strong>{format_clock(sunrise)}</strong> \u00b7 sunset <strong>{format_clock(sunset)}</strong>', f'<strong>{format_hm(hours)}</strong> of daylight ({sign}{gained:.1f} minutes from yesterday)', ]# --- data loading ---
@@ -326,7 +267,6 @@ def load_seasons(data_dir): """load season definitions from data/seasons/*.md""" seasons_dir = os.path.join(data_dir, 'seasons') seasons = [] seen = {} for filename in sorted(os.listdir(seasons_dir)): if not filename.endswith('.md'):
@@ -346,10 +286,7 @@ def load_seasons(data_dir): 'note': note, } # primary date range seasons.append(entry) if name not in seen: seen[name] = entry # winter has a second range (dec) if 'start-alt' in meta:
@@ -363,6 +300,14 @@ def load_seasons(data_dir): # sort by start month/day so lookup order is correct seasons.sort(key=lambda s: (s['start'][0], s['start'][1])) # build the canonical-order map after sorting so the nav reads # winter -> early spring -> ... -> late fall instead of alphabetical seen = {} for s in seasons: if s['name'] not in seen: seen[s['name']] = s return seasons, seen
@@ -523,132 +468,218 @@ def moon_garden_tip(phase, moon_tips): return moon_tips[-1][2] if moon_tips else ''# --- section builders ---# each builder returns a dict: {key, title, intro, groups, lore}# - groups: [{label, items: [html, ...]}] rendered as labeled bullet lists# - lore: [html, ...] rendered as short prose paragraphsdef _read_md(path, files): body = files.get(path) if not body: return None return parse_frontmatter(body)def _section_sky(now, season, data, rng): files = data['files'] intro = get_weather_mood(season['name'], get_time_of_day(now), data['moods']) lore = [] tip = moon_garden_tip(moon_phase(now), data['moon_tips']) if tip: lore.append(render_md(tip)) for path in (f"sky/{season['name']}.md", f"storms/{season['name']}.md"): parsed = _read_md(path, files) if not parsed: continue items = parse_list_items(parsed['body']) candidates = items['bullets'] + items['prose'] if candidates: pick = pick_items(candidates, 1, rng)[0] lore.append(render_md(pick)) return { 'key': 'sky', 'title': 'sky', 'intro': intro, 'groups': [{'label': '', 'items': sky_data_lines(now)}], 'lore': lore, }def _section_garden(season, data, rng): files = data['files'] groups = [] parsed = _read_md(f"planting/{season['name']}.md", files) if parsed: items = parse_list_items(parsed['body']) if items['bullets']: picks = pick_items(items['bullets'], min(4, len(items['bullets'])), rng) groups.append({ 'label': 'in the ground now', 'items': [render_md(p) for p in picks], }) parsed = _read_md(f"planting/{season['name']}-indoors.md", files) if parsed: items = parse_list_items(parsed['body']) if items['bullets']: picks = pick_items(items['bullets'], min(3, len(items['bullets'])), rng) groups.append({ 'label': 'starting indoors', 'items': [render_md(p) for p in picks], }) parsed = _read_md(f"chores/{season['name']}.md", files) if parsed: items = parse_list_items(parsed['body']) if items['bullets']: picks = pick_items(items['bullets'], min(2, len(items['bullets'])), rng) groups.append({ 'label': 'this week', 'items': [render_md(p) for p in picks], }) return {'key': 'garden', 'title': 'garden', 'intro': '', 'groups': groups, 'lore': []}def _section_kitchen(season, data, rng): files = data['files'] groups = [] parsed = _read_md(f"kitchen/{season['name']}.md", files) if parsed: items = parse_list_items(parsed['body']) bullets = list(items['bullets']) if bullets: picks = pick_items(bullets, min(4, len(bullets)), rng) groups.append({ 'label': 'in season', 'items': [render_md(p) for p in picks], }) remaining = [b for b in bullets if b not in picks] tonight = pick_items(remaining, 1, rng)[0] if remaining else picks[-1] groups.append({ 'label': 'tonight', 'items': [render_md(tonight)], }) return {'key': 'kitchen', 'title': 'kitchen', 'intro': '', 'groups': groups, 'lore': []}def _section_foraging(season, data, rng): files = data['files'] groups = [] lore = [] parsed = _read_md(f"foraging/{season['name']}.md", files) if parsed: items = parse_list_items(parsed['body']) if items['bullets']: picks = pick_items(items['bullets'], min(4, len(items['bullets'])), rng) groups.append({'label': '', 'items': [render_md(p) for p in picks]}) if items['prose']: lore.append(render_md(items['prose'][0])) return {'key': 'foraging', 'title': 'foraging', 'intro': '', 'groups': groups, 'lore': lore}def _section_folklore(season, data, rng): files = data['files'] lore = [] parsed = _read_md(f"names/{season['name']}.md", files) if parsed: items = parse_list_items(parsed['body']) if items['prose']: lore.append(render_md(items['prose'][0])) elif items['bullets']: picks = pick_items(items['bullets'], min(2, len(items['bullets'])), rng) lore.append(' '.join(render_md(p) for p in picks)) parsed = _read_md(f"remedies/{season['name']}.md", files) if parsed: items = parse_list_items(parsed['body']) parts = [] if items['bullets']: parts.append(render_md(pick_items(items['bullets'], 1, rng)[0])) if items['prose']: parts.append(render_md(items['prose'][0])) if parts: lore.append(' '.join(parts)) parsed = _read_md(f"bugs/{season['name']}.md", files) if parsed: items = parse_list_items(parsed['body']) if items['bullets']: lore.append(render_md(pick_items(items['bullets'], 1, rng)[0])) return {'key': 'folklore', 'title': 'folklore', 'intro': '', 'groups': [], 'lore': lore}SECTION_BUILDERS = [_section_sky, _section_garden, _section_kitchen, _section_foraging, _section_folklore]def render_sections_html(sections): """render the section list to a single HTML string used by both the server-side template and the JSON API response.""" parts = [] for s in sections: if not s['groups'] and not s['lore'] and not s.get('intro'): continue parts.append(f'<section class="bucket bucket-{s["key"]}">') parts.append(f'<h2>{s["title"]}</h2>') if s.get('intro'): parts.append(f'<p class="bucket-intro">{s["intro"]}</p>') for g in s['groups']: if g.get('label'): parts.append(f'<p class="bucket-label">{g["label"]}</p>') parts.append('<ul class="bucket-list">') for item in g['items']: parts.append(f'<li>{item}</li>') parts.append('</ul>') for line in s.get('lore', []): parts.append(f'<p class="bucket-lore">{line}</p>') parts.append('</section>') return ''.join(parts)# --- content assembly ---def assemble_content(now, data, season_override=None, time_override=None):def assemble_content(now, data, season_override=None): """assemble the full page content for a given moment.""" seasons = data['seasons'] seasons_map = data['seasons_map'] manifest = data['manifest'] files = data['files'] season = get_season_by_name(season_override, seasons_map) if season_override else get_season(now, seasons) real_time = get_time_of_day(now) content_time = time_override or real_time # date line date_line = written_date(now) # season header note_html = render_md_block(season['note']) nxt = days_until_next_season(now, seasons) if nxt['days'] <= 7: note_html += '<p>' + nxt['label'] + ' begins in ' + str(nxt['days']) + (' day.' if nxt['days'] == 1 else ' days.') + '</p>' # haiku haiku_lines = get_haiku(season['name'], now, data['haiku']) haiku_html = '' if haiku_lines: haiku_html = ''.join(f'<span class="haiku-line">{line}</span>' for line in haiku_lines) # moon phase for garden tip phase = moon_phase(now) # weather mood weather_mood = get_weather_mood(season['name'], content_time, data['moods']) # sky data sky_data = sky_text(now, content_time) # footer footer_text = f"{nxt['days']} days until {nxt['label']} \u00b7 zone 7a \u00b7 north carolina" # seed the rng for today rng = seeded_random(day_hash(now)) sections = [build(now, season, data, rng) if build is _section_sky else build(season, data, rng) for build in SECTION_BUILDERS] sections_html = render_sections_html(sections) # filter manifest allowed_prefixes = TIME_CONTENT.get(content_time, []) relevant = [] for entry in manifest: if 'time' in entry: if entry['time'] == content_time: relevant.append(entry) elif 'season' in entry: if entry['season'] != season['name']: continue for prefix in allowed_prefixes: if entry['path'].startswith(prefix): relevant.append(entry) break # parse entries entries = [] for entry in relevant: text = files.get(entry['path']) if text is None: continue entries.append(parse_frontmatter(text)) # collect fragments fragments = [] fragments.append(moon_garden_tip(phase, data['moon_tips'])) # wisdom lines for entry in entries: if 'time' not in entry['meta']: continue lines = [l.strip() for l in entry['body'].split('\n') if l.strip()] if not lines: continue picks = pick_items(lines, min(2, len(lines)), rng) fragments.extend(picks) # seasonal entries for entry in entries: if 'time' in entry['meta']: continue parsed = parse_list_items(entry['body']) if parsed['bullets']: picks = pick_items(parsed['bullets'], 1, rng) fragments.extend(picks) if parsed['prose']: prose_pick = pick_items(parsed['prose'], 1, rng) fragments.append(prose_pick[0]) # shuffle for i in range(len(fragments) - 1, 0, -1): j = int(rng() * (i + 1)) fragments[i], fragments[j] = fragments[j], fragments[i] # compose into paragraphs sep = ' <span class="sep">\u2767</span> ' chunk_size = min(4, max(1, math.ceil(len(fragments) / 2))) narrative_html = '' for c in range(0, len(fragments), chunk_size): chunk = fragments[c:c + chunk_size] html = sep.join(highlight_text(render_md(f)) for f in chunk) narrative_html += f'<p class="narrative">{html}</p>' # build nav html season_nav_html = build_season_nav(season, seasons_map) time_nav_html = build_time_nav(content_time, real_time) footer_text = f"{nxt['days']} days until {nxt['label']} \u00b7 zone 7a \u00b7 north carolina" return { 'date_line': date_line, 'date_line': written_date(now), 'season_name': season['label'], 'season_note': note_html, 'season_key': season['name'], 'time_key': real_time, 'content_time': content_time, 'haiku_html': haiku_html, 'weather_mood': weather_mood, 'sky_data': sky_data, 'narrative_html': narrative_html, 'sections_html': sections_html, 'footer_text': footer_text, 'season_nav_html': season_nav_html, 'time_nav_html': time_nav_html, 'season_nav_html': build_season_nav(season, seasons_map), }
@@ -660,13 +691,3 @@ def build_season_nav(active_season, seasons_map): cls = ' class="active" aria-current="true"' if s['name'] == active_season['name'] else '' html += f'<a data-season="{s["name"]}"{cls}>{s["label"]}</a>' return htmldef build_time_nav(active_time, natural_time): html = '' for t in TIME_LABELS: cls = ' class="active" aria-current="true"' if t == active_time else '' html += f'<a data-time="{t}"{cls}>{t}</a>' if active_time != natural_time: html += '<a class="return-now" data-time="now">return to now</a>' return html
@@ -21,7 +21,8 @@ DATA = load_data(DATA_DIR)@app.route('/')def index(): now = datetime.now(ZoneInfo('America/New_York')) content = assemble_content(now, DATA) season_override = request.args.get('season') content = assemble_content(now, DATA, season_override=season_override) return render_template('index.html', **content)
@@ -29,10 +30,5 @@ def index():def api_content(): now = datetime.now(ZoneInfo('America/New_York')) season_override = request.args.get('season') time_override = request.args.get('time') content = assemble_content( now, DATA, season_override=season_override, time_override=time_override, ) content = assemble_content(now, DATA, season_override=season_override) return jsonify(content)
modified
static/almanac.js
@@ -8,7 +8,6 @@ var ANIM_WORD_DELAY = 18; var ANIM_LIST_DELAY = 60; var ANIM_NAV_DELAY = 80; // --- daylight cycle ---
@@ -23,11 +22,6 @@ { name: 'night', start: 21, end: 24 } ]; var SEASONS_LIST = [ 'winter', 'early-spring', 'late-spring', 'early-summer', 'midsummer', 'early-fall', 'late-fall' ]; function getTimeOfDay(date) { var h = date.getHours(); for (var i = 0; i < TIMES.length; i++) {
@@ -36,7 +30,6 @@ return 'night'; } // season detection for auto-refresh function getSeasonName(date) { var m = date.getMonth() + 1; var d = date.getDate();
@@ -59,29 +52,33 @@ return 'winter'; } // palette per time of day. text stays in the bone/cream range so // contrast holds; the ember "heading" role drives the accent (drop // cap, roman numerals, links, mood markers) and stays warm-gold // even at night so editorial elements don't disappear. var CYCLES = { night: { bg: '#060504', text: '#9a8e80', accent: '#3a5030', heading: '#7a6a58', bg: '#0a0806', text: '#dcd0b8', accent: '#86a07a', heading: '#d2a070', glow: 'rgba(30,25,20,0.08)', under1: 'rgba(15,12,10,0.8)', under2: 'rgba(20,18,15,0.6)', under3: 'rgba(10,10,15,0.5)' }, dawn: { bg: '#0f0c09', text: '#c4b5a0', accent: '#7a9a62', heading: '#c0885a', bg: '#13100b', text: '#e0d4be', accent: '#92a880', heading: '#e3b27a', glow: 'rgba(180,120,60,0.06)', under1: 'rgba(40,25,15,0.6)', under2: 'rgba(50,30,18,0.4)', under3: 'rgba(30,20,12,0.5)' }, morning: { bg: '#0e0c0a', text: '#d4c8b8', accent: '#7a9a62', heading: '#b07a50', bg: '#14100c', text: '#e2d8c2', accent: '#92a880', heading: '#d8aa78', glow: 'rgba(140,100,50,0.04)', under1: 'rgba(26,23,20,0.7)', under2: 'rgba(42,36,32,0.5)', under3: 'rgba(26,23,20,0.4)' }, afternoon: { bg: '#0d0b09', text: '#d0c2b0', accent: '#6a8a55', heading: '#a87048', bg: '#15110e', text: '#dccfba', accent: '#88a07a', heading: '#d3a06c', glow: 'rgba(160,100,40,0.05)', under1: 'rgba(35,28,20,0.6)', under2: 'rgba(30,25,18,0.5)', under3: 'rgba(40,30,20,0.4)' }, evening: { bg: '#0b0908', text: '#c8b8a5', accent: '#5a7d4a', heading: '#b87840', bg: '#120f0b', text: '#dac9b4', accent: '#82987a', heading: '#dfa478', glow: 'rgba(180,100,40,0.08)', under1: 'rgba(45,25,12,0.7)', under2: 'rgba(35,18,10,0.6)', under3: 'rgba(25,15,8,0.5)' }
@@ -143,7 +140,7 @@ function revealWords(root) { if (!root) return; var elements = root.querySelectorAll('h2, p, li, blockquote'); var elements = root.querySelectorAll('h1, h2, p, li, blockquote'); var allItems = []; elements.forEach(function (el) {
@@ -190,10 +187,17 @@ nodes.forEach(function (n) { el.appendChild(n); }); }); var delay = 0; // cap total cascade so the last item lands within ~1s of the // first. budget includes both word and LI advances; with ~300 // items the per-item step shrinks below 5ms which still reads // as a sweep rather than a pop. var ANIM_TOTAL_MS = 900; var n = allItems.length; var step = n > 1 ? ANIM_TOTAL_MS / (n - 1) : 0; var nextDelay = 0; allItems.forEach(function (item) { item.style.animationDelay = delay + 'ms'; delay += item.tagName === 'LI' ? ANIM_LIST_DELAY : ANIM_WORD_DELAY; item.style.animationDelay = nextDelay + 'ms'; nextDelay += step; }); }
@@ -201,9 +205,7 @@ // --- main --- var currentSeasonOverride = null; var currentTimeOverride = null; var naturalSeason = null; var naturalTime = null; var dom = {}; function cacheDOM() {
@@ -211,40 +213,22 @@ dom.seasonName = document.querySelector('.season-name'); dom.seasonNote = document.querySelector('.season-note'); dom.haikuBlock = document.querySelector('.flow-haiku blockquote'); dom.weatherMoodText = document.querySelector('.weather-mood-text'); dom.skyData = document.querySelector('.sky-data'); dom.footerP = document.querySelector('footer p'); dom.wisdom = document.querySelector('.wisdom'); dom.flowEntries = document.querySelector('.flow-entries'); dom.sections = document.querySelector('.sections'); dom.footerStatus = document.querySelector('.footer-status'); dom.readout = document.querySelector('.readout'); dom.footer = document.querySelector('footer'); dom.seasonsNav = document.querySelector('.seasons-nav'); dom.timesNav = document.querySelector('.times-nav'); dom.navDrawer = document.querySelector('.nav-drawer'); dom.navToggle = document.querySelector('.nav-toggle'); if (dom.navToggle) { dom.navToggle.addEventListener('click', function () { var open = dom.navDrawer.classList.toggle('open'); dom.navToggle.textContent = open ? '...close' : '...explore'; }); } } function loadContent(seasonOverride, timeOverride) { var fadeTargets = ['.flow-season', '.flow-sky', '.flow-haiku', '.wisdom', '.flow-entries', 'footer']; function loadContent(seasonOverride) { var fadeTargets = ['.flow-season', '.flow-haiku', '.sections', 'footer']; fadeTargets.forEach(function (sel) { var el = document.querySelector(sel); if (el) el.style.opacity = '0'; }); var params = []; if (seasonOverride) params.push('season=' + encodeURIComponent(seasonOverride)); if (timeOverride) params.push('time=' + encodeURIComponent(timeOverride)); var url = '/api/content' + (params.length ? '?' + params.join('&') : ''); var url = '/api/content' + (seasonOverride ? '?season=' + encodeURIComponent(seasonOverride) : ''); currentSeasonOverride = seasonOverride; currentTimeOverride = timeOverride; fetch(url) .then(function (r) { return r.json(); })
@@ -253,20 +237,15 @@ dom.seasonName.textContent = data.season_name; dom.seasonNote.innerHTML = data.season_note; dom.haikuBlock.innerHTML = data.haiku_html; dom.weatherMoodText.innerHTML = data.weather_mood; dom.skyData.innerHTML = data.sky_data; dom.flowEntries.innerHTML = data.narrative_html; dom.footerP.textContent = data.footer_text; dom.sections.innerHTML = data.sections_html; dom.footerStatus.textContent = data.footer_text; dom.seasonsNav.innerHTML = data.season_nav_html; dom.timesNav.innerHTML = data.time_nav_html; // update body attributes for the season color document.body.setAttribute('data-season', data.season_key); document.body.setAttribute('data-time', data.time_key); // daylight cycle follows real clock, season follows content var realTime = getTimeOfDay(new Date()); applyDaylightCycle(realTime, data.season_key); // background cycle follows real clock; season tint follows content applyDaylightCycle(getTimeOfDay(new Date()), data.season_key); fadeTargets.forEach(function (sel) { var el = document.querySelector(sel);
@@ -275,11 +254,10 @@ revealWords(dom.readout); revealWords(dom.footer); bindNavClicks(); }) .catch(function () { dom.flowEntries.innerHTML = '<p style="color:var(--ash);font-style:italic;">the pages could not be found. try again in a moment.</p>'; dom.sections.innerHTML = '<p style="color:var(--ash);font-style:italic;">the pages could not be found. try again in a moment.</p>'; fadeTargets.forEach(function (sel) { var el = document.querySelector(sel); if (el) el.style.opacity = '1';
@@ -292,22 +270,7 @@ a.addEventListener('click', function () { var season = a.getAttribute('data-season'); window.scrollTo({ top: 0, behavior: 'smooth' }); if (season === naturalSeason) { loadContent(null, currentTimeOverride); } else { loadContent(season, currentTimeOverride); } }); }); dom.timesNav.querySelectorAll('a[data-time]').forEach(function (a) { a.addEventListener('click', function () { var time = a.getAttribute('data-time'); if (time === 'now') { loadContent(currentSeasonOverride, null); } else { loadContent(currentSeasonOverride, time); } loadContent(season === naturalSeason ? null : season); }); }); }
@@ -319,34 +282,27 @@ var now = new Date(); naturalSeason = getSeasonName(now); naturalTime = getTimeOfDay(now); // apply daylight cycle from server-provided attributes var initSeason = document.body.getAttribute('data-season') || naturalSeason; var initTime = document.body.getAttribute('data-time') || naturalTime; var initTime = document.body.getAttribute('data-time') || getTimeOfDay(now); applyDaylightCycle(initTime, initSeason); // reveal the server-rendered content revealWords(dom.readout); revealWords(dom.footer); // bind nav clicks on the server-rendered nav bindNavClicks(); // auto-refresh when time of day or season changes // refresh on season rollover; keep palette in sync with the clock setInterval(function () { var check = new Date(); var newTime = getTimeOfDay(check); var newSeason = getSeasonName(check); if (newTime !== naturalTime || newSeason !== naturalSeason) { naturalTime = newTime; if (newSeason !== naturalSeason) { naturalSeason = newSeason; if (!currentTimeOverride) { loadContent(currentSeasonOverride, null); if (!currentSeasonOverride) { loadContent(null); return; } // always update the daylight cycle to real time applyDaylightCycle(newTime, document.body.getAttribute('data-season') || newSeason); } applyDaylightCycle(getTimeOfDay(check), document.body.getAttribute('data-season') || newSeason); }, 60000); // register service worker for offline access
added
static/fonts/fraunces-latin-full-italic.woff2
binary file
added
static/fonts/fraunces-latin-full-normal.woff2
binary file
added
static/fonts/jetbrains-mono-latin-wght-normal.woff2
binary file
modified
static/style.css
@@ -1,20 +1,78 @@/* ================================================================ * dark furrow / stylesheet * editorial almanac. fraunces (variable, with opsz / SOFT / wonk * axes) carries the serif voice; jetbrains mono carries the data * voice. one ember accent. warm charcoal ground. svg grain over * everything for paper-by-firelight texture. * * fonts are self-hosted woff2 from @fontsource. no third-party * cdn calls at runtime. * ================================================================ */@font-face { font-family: 'Fraunces'; font-style: normal; font-weight: 100 900; font-stretch: 25% 151%; font-display: swap; src: url('/static/fonts/fraunces-latin-full-normal.woff2') format('woff2-variations');}@font-face { font-family: 'Fraunces'; font-style: italic; font-weight: 100 900; font-stretch: 25% 151%; font-display: swap; src: url('/static/fonts/fraunces-latin-full-italic.woff2') format('woff2-variations');}@font-face { font-family: 'JetBrains Mono'; font-style: normal; font-weight: 100 800; font-display: swap; src: url('/static/fonts/jetbrains-mono-latin-wght-normal.woff2') format('woff2-variations');}:root { --earth: #0e0c0a; --bark: #2a2420; --ash: #a89b8e; --bone: #d4c8b8; --parchment: #ece3d5; --sprout: #7a9a62; --moss: #5a7d4a; --ember: #b07a50; --glow: rgba(140,100,50,0.04); --under1: rgba(26,23,20,0.7); --under2: rgba(42,36,32,0.5); --under3: rgba(26,23,20,0.4); --type-title: clamp(2.8rem, 4vw + 1rem, 4.2rem); --type-season: clamp(1.6rem, 2vw + 0.6rem, 2rem); --type-body: clamp(0.95rem, 0.8vw + 0.6rem, 1.05rem); /* --- palette --------------------------------------------------- * warmer than pitch black, leaning toward burnt walnut. one * ember-gold accent does the heavy lifting; bone is body; ash * is for italic and secondary text. moss only survives for * ::selection (it's the most useful "alive" color we have). * -------------------------------------------------------------- */ --earth: #16110d; --earth-2: #1d1714; --bark: #2a2118; --ash: #8c8175; --bone: #d8cdb9; --parchment: #ebe1cc; --ember: #d4a574; --ember-warm: #c8884c; --moss: #5a7d4a; /* atmospheric layers — kept for the daylight cycle in JS */ --glow: rgba(140, 100, 50, 0.04); --under1: rgba(26, 23, 20, 0.7); --under2: rgba(42, 36, 32, 0.5); --under3: rgba(26, 23, 20, 0.4); /* --- type ----------------------------------------------------- */ --serif: 'Fraunces', 'Iowan Old Style', Georgia, 'Times New Roman', serif; --mono: 'JetBrains Mono', 'IBM Plex Mono', ui-monospace, 'SFMono-Regular', Menlo, Consolas, monospace; /* fluid type scale — set only the few that vary by viewport */ --type-wordmark: clamp(3.2rem, 5vw + 1.4rem, 5.4rem); --type-season: clamp(2.6rem, 3vw + 1.4rem, 4.2rem); --type-section: clamp(1.5rem, 1vw + 1rem, 1.9rem); --type-body: clamp(1.0rem, 0.4vw + 0.85rem, 1.12rem); --type-meta: 0.72rem; /* layout */ --measure: 36rem; --rule: 1px solid rgba(140, 110, 80, 0.18);}* {
@@ -24,428 +82,614 @@}html { font-size: 18px; font-size: 17px; scroll-behavior: smooth; text-rendering: optimizeLegibility; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;}body { background-color: var(--earth); color: var(--bone); font-family: Georgia, 'Times New Roman', serif; line-height: 1.8; -webkit-font-smoothing: antialiased; font-family: var(--serif); font-variation-settings: 'opsz' 14, 'SOFT' 50, 'wght' 380; font-feature-settings: 'liga', 'kern', 'onum'; line-height: 1.7; transition: background-color 2s ease, color 2s ease; min-height: 100vh;}main { position: relative; z-index: 1;}::selection { background-color: var(--moss); color: var(--parchment);}/* ===== ATMOSPHERIC LAYERS ======================================= * three fixed layers stacked behind the content: * - sky-layer: season radial tint (set by JS) * - time-layer: time-of-day vertical light (set by JS) * - grain: SVG noise texture for paper-by-firelight feel * ================================================================ */#sky-layer { position: fixed; inset: 0; pointer-events: none; z-index: -2; transition: background 4s ease;}#time-layer { position: fixed; inset: 0; height: 100vh; height: 100dvh; pointer-events: none; z-index: -1; transition: background 4s ease;}/* svg-noise grain. heavier than typical (~13%) because this needs to read as paper texture and firelight haze, not just polish. layered with mix-blend-mode overlay so it darkens shadows and warms highlights at the same time. */.grain { position: fixed; inset: 0; pointer-events: none; z-index: 9999; opacity: 0.07; mix-blend-mode: soft-light; background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='240' height='240' viewBox='0 0 240 240'><filter id='n'><feTurbulence type='fractalNoise' baseFrequency='0.92' numOctaves='3' stitchTiles='stitch' seed='4'/><feColorMatrix values='0 0 0 0 0.85 0 0 0 0 0.72 0 0 0 0 0.55 0 0 0 0.7 0'/></filter><rect width='100%' height='100%' filter='url(%23n)'/></svg>");}/* slow breathing warm glow. anchored above center, drifts alpha over 28s so the page pulses like a coal that won't quite go out. sits behind the season radial so it tints rather than dominates. */.glow { position: fixed; inset: 0; pointer-events: none; z-index: -3; background: radial-gradient(ellipse 60% 50% at 50% 32%, rgba(212, 165, 116, 0.12) 0%, rgba(180, 120, 60, 0.04) 30%, transparent 65%); animation: ember-breathe 28s ease-in-out infinite;}@keyframes ember-breathe { 0%, 100% { opacity: 0.4; transform: translateY(0) scale(1); } 50% { opacity: 0.8; transform: translateY(-10px) scale(1.03); }}/* ===== READOUT ===== *//* ===== READOUT (the page column) ================================ */.readout { max-width: 34rem; max-width: var(--measure); margin: 0 auto; padding: 4rem 1.5rem 2rem; padding: 3.5rem 1.6rem 2rem;}/* --- header --- *//* ===== MASTHEAD (almanac coordinate strip) ====================== * a tiny mono ribbon at the top: year roman, zone, latitude. like * a ship's log header. justified across the column. * ================================================================ */.readout-header {.masthead { display: flex; justify-content: space-between; align-items: center; font-family: var(--mono); font-size: var(--type-meta); font-weight: 400; letter-spacing: 0.18em; color: var(--ash); text-transform: uppercase; padding-bottom: 0.9rem; margin-bottom: 3rem; border-bottom: var(--rule); opacity: 0.7;}.masthead-mark:nth-child(2) { /* center mark gets a subtle ember tone */ color: var(--ember); opacity: 0.85;}.readout-header::after { display: none;/* ===== TITLE BLOCK ============================================== */.title-block { margin-bottom: 3.5rem;}.readout-header h1 { font-size: var(--type-title);.wordmark { font-family: var(--serif); font-variation-settings: 'opsz' 144, 'SOFT' 100, 'wght' 320, 'WONK' 1; font-style: normal; font-weight: 400; letter-spacing: 0.03em; font-size: var(--type-wordmark); line-height: 0.95; letter-spacing: -0.02em; color: var(--parchment); line-height: 1.1; margin-bottom: 0.4rem;}.subtitle { color: var(--ash);.wordmark-sub { font-family: var(--serif); font-variation-settings: 'opsz' 14, 'SOFT' 80, 'wght' 320; font-style: italic; font-size: var(--type-body); margin-top: 0.3rem; animation: breathe 8s ease-in-out infinite; font-size: 1rem; color: var(--ash); letter-spacing: 0.01em; animation: breathe 9s ease-in-out infinite;}/* --- date line --- *//* ===== DATE LINE (poetic) ======================================= */.date-line { color: var(--ash); font-size: var(--type-body); font-family: var(--serif); font-variation-settings: 'opsz' 14, 'SOFT' 60, 'wght' 320; font-style: italic; letter-spacing: 0.02em; margin-bottom: 1.5rem; font-size: 0.95rem; color: var(--ash); margin-bottom: 2rem; padding-left: 0.05em;}/* ===== FLOW SECTIONS ===== *//* consistent spacing and typography throughout *//* ===== SEASON BLOCK ============================================= */.flow { margin-bottom: 2.5rem; transition: opacity 0.4s ease;.season-block { margin-bottom: 3.5rem;}/* --- season --- */.season-name { font-family: var(--serif); font-variation-settings: 'opsz' 144, 'SOFT' 80, 'wght' 280; font-weight: 300; font-size: var(--type-season); font-weight: 400; color: var(--sprout); letter-spacing: 0.02em; line-height: 1.3; line-height: 1.0; letter-spacing: -0.015em; color: var(--ember); margin-bottom: 1.4rem; transition: color 2s ease;}.season-note { color: var(--ash); font-family: var(--serif); font-variation-settings: 'opsz' 18, 'SOFT' 70, 'wght' 380; font-style: italic; font-size: var(--type-body); line-height: 1.8; margin-top: 0.6rem; font-size: 1.05rem; line-height: 1.75; color: var(--bone); max-width: 30rem;}.season-note p { margin: 0; margin-bottom: 0.6rem;}/* --- sky (weather + celestial + moon tip) --- */.flow-sky p { font-size: var(--type-body); line-height: 1.9;.season-note p:last-child { margin-bottom: 0;}.weather-mood-text { color: var(--ash);/* drop cap on the very first letter — illuminated almanac feel */.season-note > p:first-child::first-letter { font-family: var(--serif); font-variation-settings: 'opsz' 144, 'SOFT' 100, 'wght' 320; font-style: italic; margin-bottom: 1rem; font-size: 3.6em; line-height: 0.85; float: left; padding: 0.18em 0.18em 0 0; margin-right: 0.04em; color: var(--ember);}.sky-data { color: var(--bone);}/* ===== HAIKU ==================================================== *//* --- haiku --- */.haiku-block { margin: 0 auto 4rem; text-align: center; padding: 1.5rem 0; position: relative;}.flow-haiku {.haiku-block::before,.haiku-block::after { content: '❦'; display: block; text-align: center; color: var(--ember); opacity: 0.35; font-size: 0.85rem; line-height: 1;}.flow-haiku blockquote {.haiku-block::before { margin-bottom: 1.4rem; }.haiku-block::after { margin-top: 1.4rem; }.haiku-block blockquote { display: inline-block; text-align: left; color: var(--ash); font-family: var(--serif); font-variation-settings: 'opsz' 24, 'SOFT' 90, 'wght' 320; font-style: italic; font-size: var(--type-body); line-height: 2.2; letter-spacing: 0.02em; font-size: 1.08rem; line-height: 2.1; color: var(--parchment); letter-spacing: 0.01em;}.flow-haiku .haiku-line {.haiku-block .haiku-line { display: block; white-space: nowrap;}.flow-haiku .haiku-line:nth-child(2) { padding-left: 1.2rem;}.flow-haiku .haiku-line:nth-child(3) { padding-left: 2.4rem;}.haiku-block .haiku-line:nth-child(2) { padding-left: 1.4rem; }.haiku-block .haiku-line:nth-child(3) { padding-left: 2.8rem; }/* --- wisdom (merged into narrative) --- *//* ===== BUCKETS (sections) ======================================= * each section: roman numeral counter, small-caps mono label, then * the content. hairline separator above each (instead of below the * heading) so the rule reads as a chapter break. * ================================================================ */.wisdom { display: none;.sections { margin-top: 1rem;}/* ===== NARRATIVE (curated items) ===== */.flow-entries {.bucket { position: relative; padding-top: 5rem; margin-bottom: 4.5rem; transition: opacity 0.4s ease;}.narrative { color: var(--bone); font-size: var(--type-body); line-height: 2; margin-bottom: 1.5rem;/* the section glyph: a single ornamental character centered above the section, varied per section so the page reads as a sequence of small ceremonies rather than a stack of cards. all from the Dingbats block so they render reliably in any font; system symbol fonts fall back to cover anything Fraunces lacks. */.bucket::before { display: block; text-align: center; font-family: 'Apple Symbols', 'Segoe UI Symbol', 'Noto Sans Symbols 2', 'Noto Sans Symbols', var(--serif); font-style: normal; font-weight: 400; font-size: 1.7rem; line-height: 1; color: var(--ember); opacity: 0.65; margin-bottom: 3.4rem; letter-spacing: 0; transition: color 1.4s ease, opacity 1.4s ease;}.narrative:last-child { margin-bottom: 0;}/* assign a glyph per section, all from U+2700-U+27BF Dingbats so they universally render as monochrome glyphs (never emoji). */.bucket-sky::before { content: '✶'; } /* six-pointed star — celestial */.bucket-garden::before { content: '❀'; } /* white florette — flower */.bucket-kitchen::before { content: '❦'; } /* floral heart — hearth */.bucket-foraging::before { content: '✿'; } /* black florette — wild plant */.bucket-folklore::before { content: '✦'; } /* four-pointed star — story */.sep { color: var(--ember); opacity: 0.4; margin: 0 0.5em; font-style: normal;.bucket:last-child { margin-bottom: 2rem;}.bucket h2 { font-family: var(--mono); font-weight: 400; font-size: var(--type-meta); letter-spacing: 0.36em; text-transform: lowercase; color: var(--ash); margin-bottom: 2rem; text-align: center; transition: color 0.4s ease;}/* --- bold highlights for scanning --- */.readout strong {.bucket h2:hover { color: var(--parchment); font-weight: 600;}/* ===== NAV DRAWER ===== */.nav-drawer { margin-top: 2.5rem;}/* --- intro line (italic mood for the SKY section) --- */.nav-toggle { display: inline-block; color: var(--ash); font-size: 0.75rem;.bucket-intro { font-family: var(--serif); font-variation-settings: 'opsz' 18, 'SOFT' 70, 'wght' 360; font-style: italic; letter-spacing: 0.02em; opacity: 0.4; cursor: pointer; padding: 0.6rem 0; min-height: 44px; line-height: 44px; transition: opacity 0.6s ease, color 0.6s ease; font-size: 1rem; line-height: 1.7; color: var(--ash); margin-bottom: 1.4rem; max-width: 30rem;}.nav-toggle:hover { opacity: 0.7; color: var(--bone);}.nav-drawer-content { display: none;/* --- group sub-labels ("in the ground now", "tonight", etc) --- */.bucket-label { font-family: var(--mono); font-size: var(--type-meta); font-weight: 400; letter-spacing: 0.22em; text-transform: uppercase; color: var(--ember-warm); margin-top: 1.6rem; margin-bottom: 0.7rem; opacity: 0.75;}.nav-drawer.open .nav-drawer-content { display: block;.bucket-label:first-child,.bucket .bucket-intro + .bucket-label { margin-top: 0;}/* ===== NAV STRIPS ===== *//* --- bullet lists --- */.nav-strip { display: flex; flex-wrap: wrap; gap: 0.2rem 0.5rem; margin-top: 1rem; opacity: 0.5; transition: opacity 0.6s ease;.bucket-list { list-style: none; padding: 0; margin: 0 0 0.4rem 0;}.nav-strip:hover,.nav-strip:focus-within { opacity: 0.8;.bucket-list li { font-family: var(--serif); font-variation-settings: 'opsz' 14, 'SOFT' 50, 'wght' 380; font-size: var(--type-body); line-height: 1.65; color: var(--bone); padding: 0.18rem 0 0.18rem 1.5rem; position: relative;}.nav-strip a { color: var(--ash); text-decoration: none; font-size: 0.75rem; font-style: italic; letter-spacing: 0.02em; padding: 0.15rem 0; transition: color 0.6s ease; cursor: pointer; opacity: 0; animation: word-in 0.4s ease-out forwards;.bucket-list li::before { content: '·'; position: absolute; left: 0.4rem; top: 0.18rem; color: var(--ember); opacity: 0.55; font-size: 1.2em; line-height: 1.4;}.nav-strip a:hover { color: var(--bone);}.nav-strip a.active { color: var(--sprout); text-decoration: underline; text-underline-offset: 3px; text-decoration-skip-ink: auto;}/* --- sky data lines (mono, no bullets — the data IS the point) --- */.nav-strip a.active::after { text-decoration: none; display: inline-block;.bucket-sky .bucket-list { font-family: var(--mono); font-size: 0.86rem; line-height: 1.95; letter-spacing: 0.02em; margin: 0.4rem 0 1rem; padding: 0.8rem 1.1rem; background: rgba(140, 100, 50, 0.04); border-left: 2px solid var(--ember);}.nav-strip a:focus-visible { outline: 1px solid var(--ember); outline-offset: 2px;.bucket-sky .bucket-list li { font-family: var(--mono); padding: 0; color: var(--bone);}.nav-strip a.return-now {.bucket-sky .bucket-list li::before { content: ''; }.bucket-sky .bucket-list li strong { color: var(--ember); flex-basis: 100%; font-weight: 500;}.nav-strip a::after { content: '\00b7';/* --- lore paragraphs (italic prose under each section) --- */.bucket-lore { font-family: var(--serif); font-variation-settings: 'opsz' 18, 'SOFT' 70, 'wght' 380; font-style: italic; font-size: 1rem; line-height: 1.8; color: var(--ash); margin-left: 0.4rem; opacity: 0.4; margin-top: 1.2rem; max-width: 32rem;}.nav-strip a:last-child::after,.nav-strip a.return-now::after { content: '';.bucket-lore:first-of-type { margin-top: 1.4rem;}.times-nav { justify-content: flex-start;/* ===== STRONG / EMPHASIS ======================================== */.readout strong { font-variation-settings: 'opsz' 14, 'SOFT' 50, 'wght' 540; color: var(--parchment); font-weight: 540;}.times-nav a.return-now { text-align: left; margin-top: 0.3rem;.bucket-lore strong { font-style: italic; color: var(--ember);}/* ===== FOOTER ===== *//* ===== FOOTER =================================================== */footer { max-width: 34rem; max-width: var(--measure); margin: 0 auto; padding: 2.5rem 1.5rem 3rem; padding: 3rem 1.6rem 4rem; text-align: center; border-top: 1px solid var(--bark); border-top: var(--rule); transition: opacity 0.4s ease, background-color 2s ease;}footer p {.footer-status { font-family: var(--mono); font-size: var(--type-meta); font-weight: 400; letter-spacing: 0.22em; text-transform: uppercase; color: var(--ash); font-size: 0.8rem; font-style: italic; letter-spacing: 0.03em; opacity: 0.75;}footer .credit { margin-top: 1rem; font-size: 0.7rem; opacity: 0.4;.seasons-nav { display: flex; flex-wrap: wrap; justify-content: center; gap: 0.4rem 1.1rem; margin-top: 1.6rem; opacity: 0.55; transition: opacity 0.6s ease;}footer .credit:hover { opacity: 0.7;.seasons-nav:hover,.seasons-nav:focus-within { opacity: 0.9;}footer .credit a { color: var(--bone);.seasons-nav a { font-family: var(--serif); font-variation-settings: 'opsz' 14, 'SOFT' 70, 'wght' 380; font-style: italic; font-size: 0.88rem; color: var(--ash); text-decoration: none; border-bottom: 1px solid var(--bark); transition: color 0.6s ease, border-color 0.6s ease; cursor: pointer; letter-spacing: 0.005em; transition: color 0.4s ease, font-variation-settings 0.4s ease; position: relative;}footer .credit a:hover { color: var(--parchment); border-color: var(--ash);.seasons-nav a:hover { color: var(--bone); font-variation-settings: 'opsz' 14, 'SOFT' 80, 'wght' 460;}.seasons-nav a.active { color: var(--ember); font-variation-settings: 'opsz' 14, 'SOFT' 80, 'wght' 460;}/* ===== SHARED ===== */.word { opacity: 0; display: inline-block; animation: word-in 0.3s ease-out forwards;.seasons-nav a:focus-visible { outline: 1px solid var(--ember); outline-offset: 4px;}.credit { margin-top: 2rem; font-family: var(--mono); font-size: 0.66rem; font-weight: 300; letter-spacing: 0.18em; text-transform: uppercase; color: var(--ash); opacity: 0.45; transition: opacity 0.6s ease;}/* ===== LAYERS ===== */.credit:hover { opacity: 0.85; }#sky-layer { position: fixed; top: 0; left: 0; right: 0; bottom: 0; pointer-events: none; z-index: 9998; transition: background 4s ease;.credit a { color: var(--bone); text-decoration: none; border-bottom: 1px solid rgba(140, 110, 80, 0.25); padding-bottom: 1px; transition: color 0.4s ease, border-color 0.4s ease;}#time-layer { position: fixed; top: 0; left: 0; right: 0; height: 100vh; height: 100dvh; pointer-events: none; z-index: 9999; transition: background 4s ease;.credit a:hover { color: var(--ember); border-color: var(--ember);}::selection { background-color: var(--moss); color: var(--parchment);}/* ===== ANIMATIONS =============================================== *//* ===== ANIMATIONS ===== */.word { opacity: 0; display: inline-block; animation: word-in 0.4s ease-out forwards;}@keyframes word-in { from { opacity: 0; filter: blur(4px); } to { opacity: 1; filter: blur(0); }.bucket-list li { opacity: 0; animation: word-in 0.45s ease-out forwards;}@keyframes breathe { 0%, 100% { opacity: 0.7; } 50% { opacity: 1; }@keyframes word-in { from { opacity: 0; filter: blur(4px); transform: translateY(2px); } to { opacity: 1; filter: blur(0); transform: translateY(0); }}@keyframes sway { 0%, 100% { transform: translateX(0); } 30% { transform: translateX(2px); } 70% { transform: translateX(-1px); }@keyframes breathe { 0%, 100% { opacity: 0.65; } 50% { opacity: 1; }}@media (prefers-reduced-motion: reduce) { *, *::before, *::after { animation-duration: 0.01ms !important; animation-iteration-count: 1 !important; transition-duration: 0.01ms !important; }}/* ===== RESPONSIVE ===== *//* ===== RESPONSIVE =============================================== */@media (max-width: 480px) { html { font-size: 16px;@media (max-width: 560px) { html { font-size: 16px; } .readout { padding: 2.2rem 1.2rem 1.5rem; } footer { padding: 2.4rem 1.2rem 3rem; } .masthead { font-size: 0.62rem; letter-spacing: 0.16em; margin-bottom: 2.2rem; } .title-block { margin-bottom: 2.6rem; } .season-block { margin-bottom: 2.6rem; } .haiku-block { margin-bottom: 3rem; } .bucket { padding-top: 2.2rem; margin-bottom: 2.6rem; } .readout { padding: 2.5rem 1.2rem 2rem; .bucket h2 { letter-spacing: 0.28em; } footer { padding: 2rem 1.2rem 3rem; .bucket-sky .bucket-list { font-size: 0.78rem; padding: 0.7rem 0.9rem; } .haiku-block .haiku-line:nth-child(2) { padding-left: 1rem; } .haiku-block .haiku-line:nth-child(3) { padding-left: 2rem; }}
modified
templates/index.html
@@ -6,7 +6,7 @@ <title>dark furrow / old knowledge preserved</title> <meta name="description" content="a seasonal almanac of planting, harvest, moon phases, and forgotten rhythms. what to grow, what to cook, what the sky is doing. tended by hand for zone 7a."> <meta name="author" content="isaac bythewood"> <meta name="theme-color" content="#0e0c0a"> <meta name="theme-color" content="#16110d"> <meta name="color-scheme" content="dark"> <!-- open graph -->
@@ -29,51 +29,49 @@ <meta name="generator" content="hand and soil"> <link rel="canonical" href="https://darkfurrow.com"> <link rel="icon" href="/static/favicon.svg" type="image/svg+xml"> <!-- typography is self-hosted from /static/fonts/ via @font-face in style.css. no third-party font cdn calls at runtime. --> <link rel="stylesheet" href="/static/style.css"></head><body data-time="{{ time_key }}" data-season="{{ season_key }}"> <div class="glow" aria-hidden="true"></div> <div id="sky-layer"></div> <div id="time-layer"></div> <div class="grain" aria-hidden="true"></div> <main> <article class="readout"> <p class="date-line">{{ date_line }}</p> <header class="readout-header"> <h1>dark furrow</h1> <p class="subtitle">old knowledge preserved</p> <header class="masthead"> <span class="masthead-mark">MMXXVI</span> <span class="masthead-mark">ZONE VII·A</span> <span class="masthead-mark">35°N</span> </header> <section class="flow flow-season"> <p class="season-name">{{ season_name }}</p> <div class="season-note">{{ season_note|safe }}</div> <section class="title-block"> <h1 class="wordmark">dark furrow</h1> <p class="wordmark-sub">old knowledge preserved</p> </section> <section class="flow flow-sky"> <p class="weather-mood-text">{{ weather_mood|safe }}</p> <p class="sky-data">{{ sky_data|safe }}</p> <p class="date-line">{{ date_line }}</p> <section class="season-block"> <p class="season-name">{{ season_name }}</p> <div class="season-note">{{ season_note|safe }}</div> </section> <section class="flow flow-haiku"> <section class="haiku-block"> <blockquote>{{ haiku_html|safe }}</blockquote> </section> <section class="wisdom"></section> <section class="flow flow-entries">{{ narrative_html|safe }}</section> <div class="nav-drawer"> <a class="nav-toggle">...explore</a> <div class="nav-drawer-content"> <nav class="nav-strip seasons-nav">{{ season_nav_html|safe }}</nav> <nav class="nav-strip times-nav">{{ time_nav_html|safe }}</nav> </div> </div> <div class="sections">{{ sections_html|safe }}</div> </article> <footer> <p>{{ footer_text }}</p> <p class="footer-status">{{ footer_text }}</p> <nav class="seasons-nav">{{ season_nav_html|safe }}</nav> <p class="credit">a project by <a href="https://isaacbythewood.com">isaac bythewood</a></p> </footer> </main>