diff --git a/.impeccable/design.json b/.impeccable/design.json
index 73267d4..a084e68 100644
--- a/.impeccable/design.json
+++ b/.impeccable/design.json
@@ -929,6 +929,14 @@
}
},
"components": [
+ {
+ "name": "The Valley",
+ "kind": "custom",
+ "refersTo": "valley",
+ "description": "The page opens on a full-width river valley (the org profile banner's scene): day in the light theme, dusk in the dark one, a CSS background via --vista, masked to fade into the page at top and bottom, with the wordmark over its sky. Drawn by assets/img/make_valley.py; shows no mod.",
+ "html": "
",
+ "css": ".ds-vista{width:100%;aspect-ratio:1280/360;min-height:230px;max-height:500px;background:url(img/valley-day.svg) 66% 100%/cover no-repeat;-webkit-mask-image:linear-gradient(to bottom,transparent 0,#000 16%,#000 88%,transparent 100%);mask-image:linear-gradient(to bottom,transparent 0,#000 16%,#000 88%,transparent 100%)}"
+ },
{
"name": "Walnut Board",
"kind": "custom",
@@ -1067,4 +1075,4 @@
"Don't rename `.card`, remove the markup hooks, or edit `data/releases.json`."
]
}
-}
+}
\ No newline at end of file
diff --git a/CLAUDE.md b/CLAUDE.md
index 51f9bb8..59aeaad 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -29,7 +29,9 @@ main).
a build takes about a minute.
- **Look:** "The Lodge Wall". Pale birch boards by day and a dark forest floor at night; each group of mods is mounted
on a walnut board with four brass nail heads; each mod is a panel (warm paper by day, moss green at night) framed in
- that mod's own colour with a picture from its own site. Wood, brown and green: Kyler rejected a black/white look.
+ that mod's own colour with a picture from its own site. The page opens on a river valley (a beaver, its lodge and
+ dam, pines and birches) with the wordmark over its sky: by day in the light theme, at dusk in the dark one. Wood,
+ brown and green: Kyler rejected a black/white look.
The look is fixed: updates extend it and never restyle it.
- **Words:** never "card", "binder", "pocket" or "sleeve" in anything a visitor reads (page text, titles, meta, alt,
aria labels). Say "mod", or write the sentence without it. The class name `.card` is read by `site.js` and the tests,
@@ -69,6 +71,10 @@ main).
as in step 6 below) `"$IMP/scripts/impeccable" embed-prompt --prompt "Origin: ..."` (reuse a neighbouring
sidecar's wording) and check with `embed-prompt --scan .`.
- The logo (header SVG in both pages and `assets/favicon.svg`) is a pine tree on a log end.
+- The valley: `assets/img/valley-day.svg` (light) and `valley-dusk.svg` (dark), a CSS background via `--vista` on
+ `.vista`, drawn by `assets/img/make_valley.py` (`cd assets/img && python make_valley.py`; vector, fixed seed, no
+ text). It is the same scene as the org profile's banner (`timbermods/.github` `profile/make_banner.py`): change the
+ two together. It shows no mod, so it never needs updating for a release.
- Mod pictures: `assets/img/cards/.webp`, 960×600, a capture of the element on that mod's own site that shows what
the mod does. When a mod's site changes it, recapture: `python scripts/card-art.py ` (no id = all eight; needs
Python with playwright + Pillow and installed Microsoft Edge; the selector per mod is in its `JOBS` table, update it
diff --git a/DESIGN.md b/DESIGN.md
index 90a23d5..c4c4878 100644
--- a/DESIGN.md
+++ b/DESIGN.md
@@ -276,7 +276,7 @@ A woodland palette: pale birch and dark forest floor for the ground, walnut and
**Character:** A wide, sturdy grotesque with the weight of a carved sign for names and headings, over a plain reading sans that stays out of the way.
### Hierarchy
-- **Display** (800, clamp(3.2rem, 7.5vw, 6rem), 0.9): the "Timber / mods" wordmark on the cover, two stacked lines, the second in pine. The 404 uses display-lost (800, clamp(2.4rem, 6vw, 4rem), 1).
+- **Display** (800, clamp(3rem, 7.2vw, 6.4rem), 0.9; clamp(2.6rem, 13vw, 3.4rem) under 620px): the "Timbermods" wordmark over the valley's sky, one line, "mods" in pine. The 404 uses display-lost (800, clamp(2.4rem, 6vw, 4rem), 1).
- **Headline** (800, clamp(1.7rem, 3.2vw, 2.4rem), 1.05): board titles and section titles.
- **Numeral** (800, 2.6rem, 1): install step numbers, in pine.
- **Title** (800, 1.35rem, 1.08): mod names on panels, the notice title, the header wordmark (at line-height 1). Title-sm (800, 1.15rem) for install step titles.
@@ -289,9 +289,9 @@ A woodland palette: pale birch and dark forest floor for the ground, walnut and
## Layout
-A single centred column 1180px wide with a fluid gutter (clamp(16px, 4vw, 32px)). The order is fixed: header bar (min 68px, rule beneath), cover, three walnut boards, install, footer.
+A single centred column 1180px wide with a fluid gutter (clamp(16px, 4vw, 32px)). The order is fixed: header bar (min 68px, rule beneath), the valley with the wordmark, cover, three walnut boards, install, footer.
-The cover is two columns (0.9fr / 1.1fr): the wordmark, lead and facts on the left, the need index on the right (rows of need and mod name, each mod name led by its colour swatch). Each board holds a head (title and lead, 760px max) and a grid of panels, three across, gap clamp(14px, 2vw, 22px), panels stretched to equal height with the download block pushed to the bottom. Where a board has a spare slot, a notice fills it (the Play together board's "Which one?"). Boards stack with clamp(24px, 4vw, 40px) between them. Install is a three-column steps sheet over three columns of notes.
+The valley runs the full width under the header (aspect 1280:360, min 230px, max 500px), with the wordmark over its sky at the top of the content column. Below it, the cover is two columns (0.9fr / 1.1fr): the lead and facts on the left, the need index on the right (rows of need and mod name, each mod name led by its colour swatch). Each board holds a head (title and lead, 760px max) and a grid of panels, three across, gap clamp(14px, 2vw, 22px), panels stretched to equal height with the download block pushed to the bottom. Where a board has a spare slot, a notice fills it (the Play together board's "Which one?"). Boards stack with clamp(24px, 4vw, 40px) between them. Install is a three-column steps sheet over three columns of notes.
Responsive: at 1000px the panels go two across and the notice spans the row; at 860px the cover, steps and notes collapse to one column and the wordmark runs on one line; at 620px the panels go one across, banners crop to 16:7, boards tighten (26px 12px 20px, 6px corners, nails inset 8px) and the download button stops wrapping. Interactive targets are at least 44px tall; download buttons 48px.
@@ -336,6 +336,9 @@ Gently rounded, hand-built shapes: boards, panels, the notice and buttons at 8px
- **Internal Padding:** 14px 16px 16px (12px 14px 14px on phones).
- **Order inside:** meta row (category and status pills), name, tagline, blurb, up to three facts with round bullets in the mod colour, then an optional caution or status line, requirements, the download block and a link row (Website first and bolder, GitHub, Release notes).
+### The valley
+The page opens on a river valley, the same scene as the org profile's banner: a beaver swimming home with a leafy branch past its stick lodge, above a dam of cut log ends, among pines and birches. It is a CSS background (`--vista`), so it follows the theme toggle: `img/valley-day.svg` in the light theme (pale sky, sun glow, clouds, sunlit water) and `img/valley-dusk.svg` in the dark one (dusk glow, moon, stars, fireflies). Both are drawn by `img/make_valley.py` (vector, fixed seed, no text). `background-position: 66% 100%` keeps the dam, the beaver and the lodge in view as narrow screens crop it. A mask fades it in from the page at the top (0 to 16%) and back out at the bottom (88% to 100%), so it sits in the ground rather than on it. It carries `role="img"` and a description. Nothing in it moves, and it shows no mod: it never needs updating when a mod changes.
+
### Walnut boards
Walnut under its grain texture, 8px corners, a thin dark edge, the Board shadow and four brass nails. The title and lead are in walnut label colours in both themes.
diff --git a/README.md b/README.md
index 4db329f..92eecdc 100644
--- a/README.md
+++ b/README.md
@@ -55,7 +55,7 @@ A card with a caution (`
Beta: …
`) also needs `data-matu
| `assets/fonts/` | Anybody, self-hosted (SIL Open Font License, `OFL-Anybody.txt`) |
| `assets/site.js` | Download buttons and theme toggle |
| `assets/img/cards/` | Each mod's picture, a capture from that mod's own site (960×600) |
-| `assets/img/` | Birch, forest-floor and walnut textures (`make_textures.py` remakes them) and the social preview `og.png` |
+| `assets/img/` | The opening valley by day and at dusk (`make_valley.py` draws them), birch, forest-floor and walnut textures (`make_textures.py`), and the social preview `og.png` |
| `data/releases.json` | Release snapshot (generated) |
| `scripts/` | `update-releases.mjs` (snapshot), `serve.mjs` (local preview), `test-site.mjs` (tests) |
| `scripts/card-art.py` | Recaptures card art from each mod's live site: `python scripts/card-art.py [card-id ...]` (Playwright, Pillow, installed Edge) |
diff --git a/assets/img/make_valley.py b/assets/img/make_valley.py
new file mode 100644
index 0000000..aea82d2
--- /dev/null
+++ b/assets/img/make_valley.py
@@ -0,0 +1,261 @@
+"""Draws the valley at the top of the page: valley-day.svg (light theme) and valley-dusk.svg (dark theme), 1280x360.
+A river valley with a beaver dam of cut log ends, a stick lodge, and a beaver swimming home with a leafy branch, among
+pines and birches. No text: the page's own title sits over the sky. The same scene (same seed, same order of drawing)
+is the org profile's banner, drawn by timbermods/.github profile/make_banner.py; change both together.
+
+Vector, procedural (fixed seed), no source images and no generative model. Run from this folder:
+ python make_valley.py
+"""
+import math, os, random
+
+HERE = os.path.dirname(os.path.abspath(__file__))
+W, H = 1280, 360
+rnd = random.Random()
+f1 = lambda v: f"{v:.1f}".rstrip("0").rstrip(".")
+
+DUSK = dict(
+ sky=[(0, "#0d1a14"), (.5, "#16281e"), (.78, "#2a3a26"), (1, "#3a3f26")],
+ glow=("#eaa94f", .62, "#b8743a", .22), night=True, clouds=False,
+ ridge="#1d3226", far="#223829", valley="#1b3023", left="#15261b",
+ pond=[(0, "#7c6c3c"), (.22, "#3f5a3f"), (1, "#1c3029")], river=[(0, "#445a3e"), (.06, "#364f39"), (1, "#1c3029")],
+ surface=("#e6c885", .55), river_surface=("#d6c28a", .35), ripple=("#cfdcc0", .22), mist=("#f3e3b8", .12),
+ bank_left="#111e17", bank_right="#18271c", grass_left="#2c4a2e", grass_right="#2f5230",
+ wake="#e6dbb8", seep="#a9cfc0", foam="#d8e8dd", grain=.9,
+ crown=["#4f7a3a", "#5f8a44", "#6f9a4a", "#557f3e", "#8fb35e", "#a3c46a"],
+)
+DAY = dict(
+ sky=[(0, "#b7d2cc"), (.45, "#d2e1d1"), (.75, "#ebdfbf"), (1, "#f2d9a6")],
+ glow=("#fff1c7", .8, "#f6d99a", .3), night=False, clouds=True,
+ ridge="#97b391", far="#89a884", valley="#56845a", left="#5f8a57",
+ pond=[(0, "#e6dcb6"), (.22, "#93b3a0"), (1, "#4d7f73")], river=[(0, "#a3bfab"), (.06, "#80a594"), (1, "#4d7f73")],
+ surface=("#fff6dc", .8), river_surface=("#fff6dc", .6), ripple=("#ffffff", .4), mist=("#ffffff", .22),
+ bank_left="#4d6e3c", bank_right="#577c44", grass_left="#76a052", grass_right="#82ad5b",
+ wake="#ffffff", seep="#eaf6f0", foam="#ffffff", grain=.5,
+ crown=["#6f9a4a", "#7fa855", "#8fb35e", "#76a04e", "#a3c46a", "#b8d47c"],
+)
+
+
+def stops(pairs):
+ return "".join(f'' for o, c in pairs)
+
+
+def pine(x, base, h, colour, tiers=3):
+ w = h * .42
+ out = [f'']
+ for i in range(tiers):
+ top = base - h + i * h * .24
+ bot = top + h * .46
+ hw = w * (.34 + .22 * i)
+ out.append(f'')
+ return "".join(out)
+
+
+def round_tree(x, base, h, colour):
+ r = h * .32
+ trunk = f''
+ blobs = "".join(f''
+ for dx, dy, s in [(0, 0, 1), (-.7, .5, .8), (.7, .45, .85), (0, .9, .9)])
+ return trunk + blobs
+
+
+def treeline(x0, x1, base_fn, hmin, hmax, colour, step=(10, 18), round_share=0.0):
+ out, x = [], x0
+ while x < x1:
+ h = rnd.uniform(hmin, hmax)
+ base = base_fn(x)
+ out.append(round_tree(x, base, h * .8, colour) if rnd.random() < round_share else pine(x, base, h, colour))
+ x += rnd.uniform(*step)
+ return "".join(out)
+
+
+def ridge_y(x):
+ y = 214 - 16 * math.sin(x / 150) - 9 * math.sin(x / 61 + 1.3)
+ if x < 470:
+ y0 = 214 - 16 * math.sin(470 / 150) - 9 * math.sin(470 / 61 + 1.3)
+ y = y0 + (470 - x) * .45
+ return y
+
+
+DAM = [(598, 306), (620, 290), (640, 270), (662, 254), (690, 247), (722, 248), (746, 254), (766, 262), (778, 306)]
+
+
+def inside(px, py, poly):
+ c = False
+ for (x1, y1), (x2, y2) in zip(poly, poly[1:] + poly[:1]):
+ if (y1 > py) != (y2 > py) and px < (x2 - x1) * (py - y1) / (y2 - y1) + x1:
+ c = not c
+ return c
+
+
+def log_end(cx, cy, r):
+ rings = "".join(f''
+ for k in (.62, .36) if r * k > 2.2)
+ return (f''
+ f'' + rings +
+ f'')
+
+
+def dam(P):
+ pts = " ".join(f"{x},{y}" for x, y in DAM)
+ placed = []
+ for _ in range(2600):
+ r = rnd.uniform(6, 14)
+ x, y = rnd.uniform(600, 776), rnd.uniform(248, 306)
+ if not inside(x, y, DAM) or any(math.hypot(x - a, y - b) < r + c - 2.5 for a, b, c in placed):
+ continue
+ placed.append((x, y, r))
+ placed.sort(key=lambda p: p[1])
+ logs = "".join(log_end(*p) for p in placed)
+ sticks = "".join(
+ f''
+ for x, y, a, L, c, sw in [(rnd.uniform(630, 760), rnd.uniform(246, 262), rnd.uniform(-3.4, -2.8) if rnd.random() < .5 else rnd.uniform(-.35, .25), rnd.uniform(16, 30),
+ rnd.choice(["#5a3d26", "#6b4a30", "#7a5536"]), rnd.uniform(2.2, 3.6)) for _ in range(7)])
+ seep = "".join(f''
+ for i, (x, y) in enumerate([(648, 268), (634, 282), (662, 262), (620, 292)]))
+ foam = "".join(f'' for x, rx in [(596, 14), (612, 8), (628, 10)])
+ return (f''
+ f'{logs}{sticks}{seep}{foam}')
+
+
+def lodge():
+ d = "M998 263 C1002 216 1038 197 1074 197 C1110 197 1146 216 1152 263 Z"
+ sticks = []
+ for _ in range(70):
+ x, y = rnd.uniform(996, 1154), rnd.uniform(196, 264)
+ a, L = rnd.uniform(-.9, .9) + (math.pi if rnd.random() < .5 else 0), rnd.uniform(16, 38)
+ c = rnd.choice(["#5a3d26", "#6b4a30", "#7d5838", "#8e6640", "#4a3120"])
+ sticks.append(f'')
+ mud = "".join(f'' for _ in range(7))
+ poke = "".join(f''
+ for x, y, dx, dy in [(1060, 202, -10, -14), (1084, 200, 12, -12), (1072, 199, 2, -17), (1026, 214, -14, -8), (1122, 214, 13, -9)])
+ leaves = "".join(f''
+ for x, y, a, c in [(1052, 190, -30, "#6f9d45"), (1094, 190, 25, "#7fae52"), (1016, 206, -40, "#5f8f3e")])
+ reflection = f''
+ return (f'{reflection}'
+ f'{"".join(sticks)}{mud}{poke}{leaves}')
+
+
+BEAVER = """
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+"""
+
+
+def wake(colour):
+ return "".join(f''
+ for x, y, dx, q, o in [(938, 266, 44, 2, .5), (975, 269, 50, 2, .38), (1018, 272, 56, 2, .26), (934, 259, 38, -2, .42),
+ (968, 256, 46, -2, .3), (842, 266, 22, 2, .45), (812, 268, 26, 2, .3)])
+
+
+def birch(x, base, h, lean, crown):
+ marks = "".join(f''
+ for y in range(int(base - h + 20), int(base - 8), 13))
+ trunk = f''
+ blobs = "".join(f''
+ for (dx, dy, rx, ry), c in zip([(-16, 8, 20, 16), (14, 4, 22, 17), (-2, -12, 22, 18), (4, 20, 18, 14), (-8, -2, 14, 11), (12, -6, 10, 8)], crown))
+ return trunk + marks + blobs
+
+
+def stump(bank):
+ return f"""
+
+
+
+
+
+"""
+
+
+def grass(x0, x1, base_fn, colour, n):
+ out = []
+ for _ in range(n):
+ x = rnd.uniform(x0, x1)
+ b = base_fn(x)
+ h = rnd.uniform(5, 12)
+ out.append(f'')
+ return "".join(out)
+
+
+def build(P, label):
+ rnd.seed(1280360) # the banner's seed and drawing order: the same valley
+ stars = "".join(f'' for _ in range(22))
+ night = ""
+ if P["night"]:
+ flies = "".join(f''
+ for x, y in [(420, 262), (468, 244), (515, 270), (560, 252), (1172, 236), (1190, 262), (944, 226), (596, 232)])
+ night = stars + ''
+ else:
+ flies = ""
+ clouds = ""
+ if P["clouds"]:
+ clouds = '' + "".join(
+ f'' for x, y, rx, ry in
+ [(760, 70, 70, 14), (800, 62, 44, 12), (1060, 44, 90, 15), (1110, 36, 50, 12), (560, 96, 60, 10), (1210, 110, 46, 9)]) + ""
+ left_bank = lambda x: 334 + 5 * math.sin(x / 70)
+ c0, o0, c1, o1 = P["glow"]
+ ripples = "".join(f''
+ for x, y, w in [(470, 314, 60), (380, 320, 44), (540, 322, 36), (1000, 290, 70), (1120, 300, 50), (860, 296, 40), (230, 318, 50)])
+ return f'''
+'''
+
+
+if __name__ == "__main__":
+ scene = "a beaver swims home with a leafy branch past its stick lodge, above a dam of logs, among pines and birches."
+ for name, P, when in [("valley-day.svg", DAY, "A river valley by day: "), ("valley-dusk.svg", DUSK, "A river valley at dusk: ")]:
+ svg = build(P, when + scene)
+ open(os.path.join(HERE, name), "w", encoding="utf-8").write(svg)
+ print("made", name, len(svg) // 1024, "KB")
diff --git a/assets/img/og.png b/assets/img/og.png
index fcfa650..1e190c6 100644
Binary files a/assets/img/og.png and b/assets/img/og.png differ
diff --git a/assets/img/valley-day.svg b/assets/img/valley-day.svg
new file mode 100644
index 0000000..f568c32
--- /dev/null
+++ b/assets/img/valley-day.svg
@@ -0,0 +1,64 @@
+
diff --git a/assets/img/valley-dusk.svg b/assets/img/valley-dusk.svg
new file mode 100644
index 0000000..7b98b4d
--- /dev/null
+++ b/assets/img/valley-dusk.svg
@@ -0,0 +1,64 @@
+
diff --git a/assets/style.css b/assets/style.css
index 2f03580..5b973fb 100644
--- a/assets/style.css
+++ b/assets/style.css
@@ -13,6 +13,7 @@
color-scheme: light;
--ground: #e0d2b4;
--ground-tex: url("img/birch.webp");
+ --vista: url("img/valley-day.svg");
--ink: #2a2118;
--muted: #5b4a37;
--rule: #bca77f;
@@ -45,6 +46,7 @@
color-scheme: dark;
--ground: #1a211a;
--ground-tex: url("img/forest-floor.webp");
+ --vista: url("img/valley-dusk.svg");
--ink: #ece3cf;
--muted: #bdb39c;
--rule: #3b4636;
@@ -70,6 +72,7 @@
color-scheme: dark;
--ground: #1a211a;
--ground-tex: url("img/forest-floor.webp");
+ --vista: url("img/valley-dusk.svg");
--ink: #ece3cf;
--muted: #bdb39c;
--rule: #3b4636;
@@ -129,12 +132,18 @@ code { font-family: var(--mono); font-size: .9em; background: var(--code-bg); pa
:root:not([data-theme="light"]) .theme-toggle .moon { display: block; }
}
-/* ---------- the opening ---------- */
-.cover { padding: clamp(32px, 5vw, 64px) 0 clamp(28px, 4vw, 48px); }
-.cover-grid { display: grid; grid-template-columns: minmax(0, .9fr) minmax(0, 1.1fr); gap: clamp(24px, 5vw, 64px); align-items: start; }
-.cover h1 { font: 800 clamp(3.2rem, 7.5vw, 6rem)/.9 var(--display); letter-spacing: -.03em; margin: 0 0 .25em; overflow-wrap: normal; }
-.cover h1 span { display: block; }
+/* ---------- the opening: the valley, with the title over its sky ---------- */
+.cover { padding: 0 0 clamp(28px, 4vw, 48px); }
+.vista-band { position: relative; }
+/* the same valley as the org profile's banner: by day in the light theme, at dusk in the dark one. It fades in from
+ the page at the top and back into it at the bottom, so it sits in the page rather than on it. */
+.vista { width: 100%; aspect-ratio: 1280 / 360; min-height: 230px; max-height: 500px; background: var(--vista) 66% 100% / cover no-repeat;
+ -webkit-mask-image: linear-gradient(to bottom, transparent 0, #000 16%, #000 88%, transparent 100%);
+ mask-image: linear-gradient(to bottom, transparent 0, #000 16%, #000 88%, transparent 100%); }
+.vista-title { position: absolute; inset: 0; padding-top: clamp(28px, 5.4vw, 78px); pointer-events: none; }
+.cover h1 { font: 800 clamp(3rem, 7.2vw, 6.4rem)/.9 var(--display); letter-spacing: -.03em; margin: 0; overflow-wrap: normal; white-space: nowrap; }
.cover h1 span + span { color: var(--link); }
+.cover-grid { display: grid; grid-template-columns: minmax(0, .9fr) minmax(0, 1.1fr); gap: clamp(24px, 5vw, 64px); align-items: start; margin-top: clamp(8px, 2vw, 24px); }
.lead { font-size: 1.15rem; max-width: 46ch; }
.cover-facts { color: var(--muted); max-width: 46ch; margin: 0; }
.needs h2 { font: 700 1.05rem/1.2 var(--display); letter-spacing: .01em; margin: 0 0 .5em; }
@@ -238,10 +247,10 @@ code { font-family: var(--mono); font-size: .9em; background: var(--code-bg); pa
}
@media (max-width: 860px) {
.cover-grid, .steps, .notes { grid-template-columns: minmax(0, 1fr); }
- .cover h1 span { display: inline; }
.steps li + li { border-left: 0; border-top: 1px solid var(--tile-rule); }
}
@media (max-width: 620px) {
+ .cover h1 { font-size: clamp(2.6rem, 13vw, 3.4rem); }
.pockets { grid-template-columns: minmax(0, 1fr); }
.banner img { aspect-ratio: 16 / 7; }
.page { padding: 26px 12px 20px; border-radius: 6px; }
diff --git a/index.html b/index.html
index 925eea7..0516d06 100644
--- a/index.html
+++ b/index.html
@@ -44,9 +44,12 @@
+
+
+
Timbermods
+
-
Timbermods
Unofficial mods for Timberborn, made by players. Each one has a download for its latest release and a link to its own site.
Eight mods for Timberborn 1.1.2.4. Most are made to work in BeaverBuddies co-op.