design: favicon v2 (V-centric) + logo-placement mockups
CI / test (push) Has been cancelled

Iteration after the atmospheric-redesign checkpoint. The earlier A/B/C/D
favicon set was scrapped — generic invented icons (roof, frame+horizon,
skyline, wax seal) that didn't tie to the brand's own glyph. New direction:
the yellow V from the wordmark, presented four ways. V-viewfinder (a V cut
out of navy showing the harbor photo inside) is the chosen path; refinement
3b crops to Camogli's coloured row-houses for a more brand-specific small
size reading.

Also adds three logo-placement mockups (PWA cold-launch splash, library
hero with the full wordmark logo, settings → about page) to give the
wordmark room to live beyond emails and login badges.

Self-contained: assets/ and spa/ copied into the design folder so the
mockups render without depending on neighbouring directories.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-15 10:25:09 -04:00
parent 01b6007b1c
commit 3bd0b9a5a1
49 changed files with 2120 additions and 0 deletions
+65
View File
@@ -0,0 +1,65 @@
# WeVisto · favicons v2 + logo placements
2026-05-15 iteration following the atmospheric-redesign checkpoint.
Two threads:
1. **Favicons rebuilt** — scrapped the previous A/B/C/D (roof, frame+horizon,
skyline, wax seal) as generic invented icons. The new set reduces to the
brand's own glyph: the yellow V from the wordmark, presented four ways.
2. **The wordmark logo placed at hero scale** — three mockups giving the
full `wordmark.svg` (harbor photo + "WeVisto") room to live in the PWA
beyond the existing email + small-badge usage.
## Picked direction (favicons)
**3 · V-viewfinder.** The V cut out of navy with the harbor photo visible
inside — *you are looking at a photograph framed by the V.* Strongest
refinement is **3b** (`favicons/3b-viewfinder-buildings-*.png`) which tightens
the inner photo crop onto Camogli's coloured row-houses for a more
brand-specific small-size reading.
Runner-up: **4 · V-cropped** (`favicons/4-V-cropped-*.png`) — an actual
tight crop of the published wordmark. Most "WeVisto in the image itself"
of any direction.
## What's here
```
favicons-and-logo-v2/
├── index.html — entry point, shows everything
├── favicons/
│ ├── 1-V-pure-{16,32,64,180}.png — yellow V on navy, isolated
│ ├── 2-V-horizon-{...}.png — V plus horizon + sun
│ ├── 3-V-viewfinder-{...}.png — original V cut over full harbor
│ ├── 3a-viewfinder-full-thin-{...}.png — refinement A (full harbor, thin frame)
│ ├── 3b-viewfinder-buildings-{...}.png — recommended: Camogli row-houses
│ ├── 3c-viewfinder-noframe-{...}.png — no yellow outline
│ ├── 3d-viewfinder-thick-{...}.png — thick yellow frame
│ ├── 4-V-cropped-{...}.png — cropped from actual wordmark
│ └── 2-V-horizon.svg — SVG source for direction 2
├── logo-moments/
│ ├── splash.html — PWA cold-launch full-screen
│ ├── library-with-logo.html — library hero w/ full wordmark
│ └── about.html — settings → about page
├── assets/ — harbor.jpg, wordmark.svg, mark images
└── spa/ — shared atmospheric tokens + chrome
(so logo-moments can render)
```
## To view
```bash
cd webApp/_design/favicons-and-logo-v2
python3 -m http.server 8766 --bind 0.0.0.0
# → open http://<host>:8766/
```
## Open work
- Decide between **3b** (recommended) and **4** (also "promising").
- Logo-moments not yet picked — Matt's review pending on which of the
three placements (splash / library hero / about page) to wire up first.
- Once selected, rasterize 3b at the standard PWA sizes (16/32/64 favicons,
180 apple-touch-icon, 192/512 PWA manifest icons, 512 maskable variant
with safe-zone padding) and ship into `webApp/frontend/public/icons/`
replacing the current split-W-on-photo set.
Binary file not shown.

After

Width:  |  Height:  |  Size: 264 KiB

@@ -0,0 +1,12 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
<defs>
<clipPath id="wv-left"><rect x="0" y="0" width="32" height="64"/></clipPath>
<clipPath id="wv-right"><rect x="32" y="0" width="32" height="64"/></clipPath>
</defs>
<rect width="64" height="64" rx="12" fill="#1a3a5c"/>
<g font-family="system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif"
font-weight="900" font-size="56" text-anchor="middle">
<text x="32" y="50" fill="#fafafa" clip-path="url(#wv-left)">W</text>
<text x="32" y="50" fill="#f0d000" clip-path="url(#wv-right)">W</text>
</g>
</svg>

After

Width:  |  Height:  |  Size: 599 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 234 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 352 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 365 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 637 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 377 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 541 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 857 B

@@ -0,0 +1,13 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
<rect width="64" height="64" fill="#0e2740"/>
<!-- full V — both strokes clearly meeting at bottom -->
<path d="M 14 14 L 32 50 L 50 14"
stroke="#f0d000" stroke-width="9"
stroke-linejoin="round" stroke-linecap="round" fill="none"/>
<!-- horizon line + sun below the V, like the sea visible past the wordmark -->
<line x1="6" y1="58" x2="58" y2="58"
stroke="#f0d000" stroke-width="1.4" opacity="0.5"/>
<circle cx="50" cy="58" r="2.4" fill="#f0d000"/>
</svg>

After

Width:  |  Height:  |  Size: 558 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 647 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 647 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 648 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 491 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 675 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 795 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.7 KiB

+301
View File
@@ -0,0 +1,301 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>WeVisto — favicons v2 + logo moments</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Nunito:wght@400;600;700;900&family=Marcellus&family=Cormorant+Garamond:ital,wght@1,400&family=DM+Mono:wght@400;500&display=swap" rel="stylesheet">
<style>
:root {
--cream: #fdf6ee; --cream-deep: #f5ead4;
--ink: #2a1f15; --ink-soft: #6e6450;
--navy: #0e2740; --yellow: #f0d000;
--rule: rgba(42,31,21,0.15);
}
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
html, body { background: var(--cream); color: var(--ink); }
body { font-family: 'Nunito', sans-serif; font-size: 16px; line-height: 1.55; padding: 48px 40px 96px; max-width: 1280px; margin: 0 auto; }
.top { display: grid; grid-template-columns: auto 1fr auto; align-items: end; gap: 24px; padding-bottom: 14px; border-bottom: 1px solid var(--rule); }
.top .l, .top .r { font-family: 'DM Mono', monospace; font-size: 10px; letter-spacing: 0.28em; text-transform: uppercase; color: var(--ink-soft); }
.top .r { text-align: right; }
.top .name { font-family: 'Marcellus', serif; font-size: 44px; color: var(--navy); text-align: center; letter-spacing: 0.01em; }
.top .name .v { color: var(--yellow); }
.subhead { margin-top: 12px; padding-bottom: 22px; border-bottom: 1px solid var(--rule); text-align: center; font-family: 'Cormorant Garamond', serif; font-style: italic; font-size: 21px; color: var(--ink); }
h2.section { margin: 56px 0 8px; font-family: 'Marcellus', serif; font-weight: 400; font-size: 30px; color: var(--navy); }
h2.section .num { color: var(--yellow); margin-right: 8px; }
.lede { font-family: 'Cormorant Garamond', serif; font-style: italic; font-size: 19px; color: var(--ink); margin-bottom: 28px; max-width: 76ch; line-height: 1.5; }
.lede em { color: var(--navy); }
.lede strong { font-weight: 600; font-style: normal; color: var(--navy); }
/* favicon grid — 2x2, each shown at all four sizes + browser-tab + ios-tile */
.fav-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 28px; margin-top: 32px; }
.fav { background: #fff9ee; border: 1px solid var(--rule); padding: 24px; }
.fav__head { display: flex; align-items: baseline; justify-content: space-between; margin-bottom: 12px; }
.fav__num { font-family: 'Marcellus', serif; font-size: 28px; color: var(--yellow); line-height: 1; }
.fav__tag { font-family: 'DM Mono', monospace; font-size: 10px; letter-spacing: 0.26em; text-transform: uppercase; color: var(--ink-soft); }
.fav h3 { font-family: 'Marcellus', serif; font-weight: 400; font-size: 24px; color: var(--navy); margin-bottom: 6px; }
.fav .desc { font-family: 'Cormorant Garamond', serif; font-style: italic; font-size: 17px; color: var(--ink); margin-bottom: 18px; line-height: 1.45; }
.fav .desc em { color: var(--navy); font-style: italic; }
.fav .row {
display: flex; align-items: flex-end; gap: 18px;
padding: 14px 0; border-top: 1px solid var(--rule); border-bottom: 1px solid var(--rule);
margin-bottom: 14px;
}
.fav .pip { text-align: center; }
.fav .pip img { display: block; image-rendering: pixelated; margin: 0 auto 6px; border-radius: 3px; }
.fav .pip .cap { font-family: 'DM Mono', monospace; font-size: 9px; letter-spacing: 0.2em; color: var(--ink-soft); }
.fav .ctx { display: grid; grid-template-columns: 1fr 1fr; gap: 14px; }
.fav .ctx .box { padding: 12px; background: var(--cream); border: 1px solid var(--rule); }
.fav .ctx .box-label { font-family: 'DM Mono', monospace; font-size: 9px; letter-spacing: 0.22em; text-transform: uppercase; color: var(--ink-soft); margin-bottom: 8px; }
.tab-strip { background: #2a2a2a; border-radius: 4px 4px 0 0; padding: 4px 4px 0; display: inline-block; }
.tab-strip .tab { display: inline-flex; align-items: center; gap: 6px; background: #3a3a3a; padding: 5px 10px 5px 8px; border-radius: 4px 4px 0 0; font-family: -apple-system, sans-serif; font-size: 11px; color: #ddd; }
.tab-strip .tab img { width: 14px; height: 14px; }
.ios-tile { width: 56px; height: 56px; border-radius: 12px; overflow: hidden; box-shadow: 0 4px 10px rgba(0,0,0,0.25); }
.ios-tile img { width: 100%; height: 100%; display: block; }
/* logo-moment cards */
.stack { display: grid; gap: 32px; margin-top: 24px; grid-template-columns: repeat(3, 1fr); }
.moment { background: #fff9ee; border: 1px solid var(--rule); overflow: hidden; }
.moment .preview { position: relative; aspect-ratio: 9/16; background: #06121f; }
.moment .preview iframe { border: 0; width: 250%; height: 250%; transform: scale(0.4); transform-origin: top left; display: block; }
.moment .open { position: absolute; bottom: 12px; right: 12px; z-index: 5; font-family: 'DM Mono', monospace; font-size: 10px; letter-spacing: 0.28em; text-transform: uppercase; color: #fff; background: rgba(7,23,42,0.75); padding: 8px 14px; text-decoration: none; backdrop-filter: blur(6px); }
.moment .open:hover { background: var(--yellow); color: var(--navy); }
.moment .body { padding: 22px 24px 24px; }
.moment .body .roman { font-family: 'Marcellus', serif; font-size: 22px; color: var(--yellow); margin-bottom: 4px; }
.moment .body h3 { font-family: 'Marcellus', serif; font-weight: 400; font-size: 22px; color: var(--navy); margin-bottom: 6px; }
.moment .body p { font-size: 15px; line-height: 1.5; color: var(--ink); }
.moment .body p em { color: var(--navy); font-style: italic; }
.also { margin-top: 56px; padding: 16px 20px; background: rgba(240,208,0,0.18); border-left: 4px solid var(--yellow); font-style: italic; color: var(--ink); font-size: 15px; }
.also a { color: var(--navy); border-bottom: 1px solid var(--navy); text-decoration: none; }
footer { margin-top: 64px; padding-top: 22px; border-top: 1px solid var(--rule); text-align: center; font-family: 'Cormorant Garamond', serif; font-style: italic; font-size: 14px; color: var(--ink-soft); }
@media (max-width: 900px) {
body { padding: 32px 20px; }
.fav-grid, .stack { grid-template-columns: 1fr; }
}
</style>
</head>
<body>
<div class="top">
<div class="l">v2 · 2026-05-15</div>
<div class="name">We<span class="v">V</span>isto</div>
<div class="r">Favicons rethought<br>+ logo placed</div>
</div>
<div class="subhead">— scrapping the previous favicon set; the brand's own V is the answer, not invented icons.</div>
<!-- FAVICONS — V2 -->
<h2 class="section"><span class="num">I.</span>Favicons, rebuilt around the V — <span style="color: var(--navy); font-style: italic; font-family: 'Cormorant Garamond', serif;">V-viewfinder picked, refining</span></h2>
<p class="lede">Direction 3 (V-viewfinder) is the chosen path — the V as a window onto the harbor. Four refinements of it below: <strong>3b is the strongest</strong> (tighter crop on Camogli's coloured row-houses + thin yellow frame). 3a, 3c, 3d shown for comparison.</p>
<style>
.v3-variants { display: grid; grid-template-columns: repeat(4, 1fr); gap: 18px; margin-top: 24px; margin-bottom: 48px; }
.v3-card { background: #fff9ee; border: 1px solid var(--rule); padding: 18px; }
.v3-card.pick { background: #fffbe0; border-color: var(--yellow); box-shadow: 0 0 0 1px var(--yellow); }
.v3-card .pick-tag { display: inline-block; font-family: 'DM Mono', monospace; font-size: 9px; letter-spacing: 0.26em; text-transform: uppercase; color: var(--yellow); background: var(--navy); padding: 3px 8px; margin-bottom: 8px; }
.v3-card h4 { font-family: 'Marcellus', serif; font-weight: 400; font-size: 18px; color: var(--navy); margin-bottom: 4px; }
.v3-card .desc { font-family: 'Cormorant Garamond', serif; font-style: italic; font-size: 14px; line-height: 1.4; color: var(--ink); margin-bottom: 12px; min-height: 56px; }
.v3-card .row { display: flex; align-items: flex-end; gap: 10px; padding: 10px 0; border-top: 1px solid var(--rule); }
.v3-card .row img { display: block; image-rendering: pixelated; border-radius: 2px; }
.v3-card .row .cap { font-family: 'DM Mono', monospace; font-size: 8px; letter-spacing: 0.18em; color: var(--ink-soft); text-align: center; margin-top: 4px; }
</style>
<div class="v3-variants">
<div class="v3-card">
<h4>3a · full harbor</h4>
<div class="desc">Full harbor view inside the V. Detailed but busy at small sizes.</div>
<div class="row">
<div><img src="favicons/3a-viewfinder-full-thin-16.png" width="28" height="28"><div class="cap">16</div></div>
<div><img src="favicons/3a-viewfinder-full-thin-32.png" width="44" height="44"><div class="cap">32</div></div>
<div><img src="favicons/3a-viewfinder-full-thin-64.png" width="64" height="64"><div class="cap">64</div></div>
<div><img src="favicons/3a-viewfinder-full-thin-180.png" width="90" height="90"><div class="cap">180</div></div>
</div>
</div>
<div class="v3-card pick">
<span class="pick-tag">Recommended</span>
<h4>3b · Camogli row-houses</h4>
<div class="desc">Tighter crop on the colored row-houses. Most brand-specific — <em>Camogli is the brand origin.</em></div>
<div class="row">
<div><img src="favicons/3b-viewfinder-buildings-16.png" width="28" height="28"><div class="cap">16</div></div>
<div><img src="favicons/3b-viewfinder-buildings-32.png" width="44" height="44"><div class="cap">32</div></div>
<div><img src="favicons/3b-viewfinder-buildings-64.png" width="64" height="64"><div class="cap">64</div></div>
<div><img src="favicons/3b-viewfinder-buildings-180.png" width="90" height="90"><div class="cap">180</div></div>
</div>
</div>
<div class="v3-card">
<h4>3c · no frame</h4>
<div class="desc">No yellow outline. Cleanest silhouette but <em>loses the brand yellow at small sizes.</em></div>
<div class="row">
<div><img src="favicons/3c-viewfinder-noframe-16.png" width="28" height="28"><div class="cap">16</div></div>
<div><img src="favicons/3c-viewfinder-noframe-32.png" width="44" height="44"><div class="cap">32</div></div>
<div><img src="favicons/3c-viewfinder-noframe-64.png" width="64" height="64"><div class="cap">64</div></div>
<div><img src="favicons/3c-viewfinder-noframe-180.png" width="90" height="90"><div class="cap">180</div></div>
</div>
</div>
<div class="v3-card">
<h4>3d · thick frame</h4>
<div class="desc">Heavier yellow border, more "picture frame" reading. Veers toward decorative.</div>
<div class="row">
<div><img src="favicons/3d-viewfinder-thick-16.png" width="28" height="28"><div class="cap">16</div></div>
<div><img src="favicons/3d-viewfinder-thick-32.png" width="44" height="44"><div class="cap">32</div></div>
<div><img src="favicons/3d-viewfinder-thick-64.png" width="64" height="64"><div class="cap">64</div></div>
<div><img src="favicons/3d-viewfinder-thick-180.png" width="90" height="90"><div class="cap">180</div></div>
</div>
</div>
</div>
<p class="lede">Original first-round directions below for reference (1 V-pure, 2 V-horizon, 4 V-cropped).</p>
<div class="fav-grid">
<div class="fav">
<div class="fav__head"><span class="fav__num">1.</span><span class="fav__tag">Brand · isolated</span></div>
<h3>V-pure</h3>
<div class="desc">The yellow V on solid navy, drawn confidently. <em>The brand's own glyph, alone in a room.</em> Reads at every size, no decoration.</div>
<div class="row">
<div class="pip"><img src="favicons/1-V-pure-16.png" width="32" height="32"><div class="cap">16</div></div>
<div class="pip"><img src="favicons/1-V-pure-32.png" width="48" height="48"><div class="cap">32</div></div>
<div class="pip"><img src="favicons/1-V-pure-64.png" width="64" height="64"><div class="cap">64</div></div>
<div class="pip"><img src="favicons/1-V-pure-180.png" width="96" height="96"><div class="cap">180</div></div>
</div>
<div class="ctx">
<div class="box">
<div class="box-label">browser tab</div>
<div class="tab-strip"><span class="tab"><img src="favicons/1-V-pure-32.png">WeVisto</span></div>
</div>
<div class="box">
<div class="box-label">iOS home screen</div>
<div class="ios-tile"><img src="favicons/1-V-pure-180.png"></div>
</div>
</div>
</div>
<div class="fav">
<div class="fav__head"><span class="fav__num">2.</span><span class="fav__tag">Brand · with horizon</span></div>
<h3>V-horizon</h3>
<div class="desc">The V plus a hairline horizon and a single sun dot below — <em>the harbor is implied,</em> not depicted. Editorial echo of the rest of the brand.</div>
<div class="row">
<div class="pip"><img src="favicons/2-V-horizon-16.png" width="32" height="32"><div class="cap">16</div></div>
<div class="pip"><img src="favicons/2-V-horizon-32.png" width="48" height="48"><div class="cap">32</div></div>
<div class="pip"><img src="favicons/2-V-horizon-64.png" width="64" height="64"><div class="cap">64</div></div>
<div class="pip"><img src="favicons/2-V-horizon-180.png" width="96" height="96"><div class="cap">180</div></div>
</div>
<div class="ctx">
<div class="box">
<div class="box-label">browser tab</div>
<div class="tab-strip"><span class="tab"><img src="favicons/2-V-horizon-32.png">WeVisto</span></div>
</div>
<div class="box">
<div class="box-label">iOS home screen</div>
<div class="ios-tile"><img src="favicons/2-V-horizon-180.png"></div>
</div>
</div>
</div>
<div class="fav">
<div class="fav__head"><span class="fav__num">3.</span><span class="fav__tag">Brand · viewfinder</span></div>
<h3>V-viewfinder</h3>
<div class="desc">The V cut out of navy, with the harbor photo visible inside — <em>you are looking at a photograph framed by the V.</em> Strong at 64+, silhouette holds at 16.</div>
<div class="row">
<div class="pip"><img src="favicons/3-V-viewfinder-16.png" width="32" height="32"><div class="cap">16</div></div>
<div class="pip"><img src="favicons/3-V-viewfinder-32.png" width="48" height="48"><div class="cap">32</div></div>
<div class="pip"><img src="favicons/3-V-viewfinder-64.png" width="64" height="64"><div class="cap">64</div></div>
<div class="pip"><img src="favicons/3-V-viewfinder-180.png" width="96" height="96"><div class="cap">180</div></div>
</div>
<div class="ctx">
<div class="box">
<div class="box-label">browser tab</div>
<div class="tab-strip"><span class="tab"><img src="favicons/3-V-viewfinder-32.png">WeVisto</span></div>
</div>
<div class="box">
<div class="box-label">iOS home screen</div>
<div class="ios-tile"><img src="favicons/3-V-viewfinder-180.png"></div>
</div>
</div>
</div>
<div class="fav">
<div class="fav__head"><span class="fav__num">4.</span><span class="fav__tag">Brand · cropped</span></div>
<h3>V-cropped (the wordmark itself)</h3>
<div class="desc">A tight crop of the actual published wordmark. The favicon <em>is</em> a slice of the logo — V centered, "e" and "i" partially visible at large sizes. The most "WeVisto in the image."</div>
<div class="row">
<div class="pip"><img src="favicons/4-V-cropped-16.png" width="32" height="32"><div class="cap">16</div></div>
<div class="pip"><img src="favicons/4-V-cropped-32.png" width="48" height="48"><div class="cap">32</div></div>
<div class="pip"><img src="favicons/4-V-cropped-64.png" width="64" height="64"><div class="cap">64</div></div>
<div class="pip"><img src="favicons/4-V-cropped-180.png" width="96" height="96"><div class="cap">180</div></div>
</div>
<div class="ctx">
<div class="box">
<div class="box-label">browser tab</div>
<div class="tab-strip"><span class="tab"><img src="favicons/4-V-cropped-32.png">WeVisto</span></div>
</div>
<div class="box">
<div class="box-label">iOS home screen</div>
<div class="ios-tile"><img src="favicons/4-V-cropped-180.png"></div>
</div>
</div>
</div>
</div>
<!-- LOGO MOMENTS -->
<h2 class="section"><span class="num">II.</span>The wordmark logo, given room to live</h2>
<p class="lede">Currently the full <em>wordmark + harbor</em> logo only appears in email templates and as a small badge on Twig pages. These three mockups give it three new homes in the PWA at <strong>hero scale</strong> — each one a moment where the brand earns the room.</p>
<div class="stack">
<div class="moment">
<div class="preview">
<iframe src="logo-moments/splash.html" loading="lazy"></iframe>
<a class="open" href="logo-moments/splash.html" target="_blank">Open ⟶</a>
</div>
<div class="body">
<div class="roman">II·a</div>
<h3>PWA cold-launch splash</h3>
<p>Shown for ~1.5s on app cold-launch. Wordmark logo at ~520px, Ken-Burns harbor backdrop, three-dot loader. Then it fades to <em>HomeView.</em></p>
</div>
</div>
<div class="moment">
<div class="preview">
<iframe src="logo-moments/library-with-logo.html" loading="lazy"></iframe>
<a class="open" href="logo-moments/library-with-logo.html" target="_blank">Open ⟶</a>
</div>
<div class="body">
<div class="roman">II·b</div>
<h3>Library hero (revised)</h3>
<p>The empty state's 112px mark replaced by the <em>full wordmark logo</em> at 320px. Same headline / sub / CTA below. The library page becomes a brand moment.</p>
</div>
</div>
<div class="moment">
<div class="preview">
<iframe src="logo-moments/about.html" loading="lazy"></iframe>
<a class="open" href="logo-moments/about.html" target="_blank">Open ⟶</a>
</div>
<div class="body">
<div class="roman">II·c</div>
<h3>About · the brand story</h3>
<p>A settings → about page with the wordmark logo as page hero, a two-section editorial about the frame and the V, then a credits card. <em>The place where the brand explains itself.</em></p>
</div>
</div>
</div>
<div class="also">Saved iteration: this is in <code>/tmp/wevisto-mockups/v2/</code> for now. Approve a direction (or set of directions) and I'll save it into the repo alongside <a href="../">the previous checkpoint</a>.</div>
<footer>
Open each tile to interact at full scale.
</footer>
</body>
</html>
@@ -0,0 +1,173 @@
<!doctype html>
<html lang="en" data-theme="ocean-dusk">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
<title>About — WeVisto</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Nunito:wght@400;600;700;900&family=Marcellus&family=Cormorant+Garamond:ital,wght@1,400&family=DM+Mono:wght@400;500&display=swap" rel="stylesheet">
<link rel="stylesheet" href="../spa/_tokens.css">
<link rel="stylesheet" href="../spa/_chrome.css">
<style>
.about-hero {
padding: var(--space-5) var(--space-5) var(--space-6);
display: flex;
flex-direction: column;
align-items: center;
text-align: center;
position: relative;
}
.about-hero__plate {
font: var(--type-label);
letter-spacing: 0.32em;
text-transform: uppercase;
color: var(--text-muted);
margin-bottom: var(--space-4);
display: flex; align-items: center; gap: 10px;
}
.about-hero__plate .roman { color: var(--brand-yellow); font-family: var(--font-display); font-size: var(--text-md); }
.about-hero__plate .sep { width: 24px; height: 1px; background: var(--glass-bord); }
/* Wordmark logo as the page hero */
.about-hero__logo {
width: min(70vw, 280px);
aspect-ratio: 1;
border-radius: 18px;
overflow: hidden;
margin-bottom: var(--space-5);
box-shadow:
0 1px 0 rgba(255,255,255,0.18) inset,
0 22px 44px -12px rgba(0,0,0,0.6);
}
.about-hero__logo img { width: 100%; height: 100%; display: block; }
.about-hero__title {
font: var(--type-display-lg);
color: var(--text);
margin-bottom: var(--space-2);
}
.about-hero__sub {
font: italic 400 17px/1.55 var(--font-accent);
color: var(--text-muted);
max-width: 38ch;
}
.about-hero__rule {
width: 80px; height: 1px;
background: linear-gradient(90deg, transparent, var(--brand-yellow), transparent);
margin-top: var(--space-5);
}
.story {
margin: var(--space-6) var(--space-5);
padding: var(--space-5) var(--space-5) var(--space-6);
border-radius: var(--radius-lg);
}
.story h2 {
font: var(--type-display-md);
color: var(--text);
margin-bottom: var(--space-3);
display: flex;
align-items: baseline;
gap: 10px;
}
.story h2 .roman {
font-family: var(--font-display);
color: var(--brand-yellow);
font-size: var(--text-lg);
}
.story p {
font-size: var(--text-base);
line-height: 1.65;
color: var(--text);
margin-bottom: var(--space-3);
}
.story p em { font-family: var(--font-accent); font-style: italic; color: var(--brand-yellow); }
.story p strong { font-family: var(--font-display); font-weight: 400; color: var(--text); }
/* the credit list as a glass card */
.credits {
margin: var(--space-6) var(--space-5);
padding: var(--space-5);
border-radius: var(--radius-lg);
}
.credits h3 {
font: var(--type-label);
letter-spacing: 0.32em;
text-transform: uppercase;
color: var(--text-muted);
margin-bottom: var(--space-4);
}
.credits dl { display: grid; grid-template-columns: auto 1fr; gap: var(--space-3) var(--space-5); }
.credits dt {
font-family: var(--font-display);
font-size: var(--text-sm);
color: var(--brand-yellow);
letter-spacing: 0.04em;
}
.credits dd { font-family: var(--font-accent); font-style: italic; font-size: var(--text-base); color: var(--text); }
.signature { margin: var(--space-6) var(--space-5) 0; }
</style>
</head>
<body>
<div class="atmosphere"></div>
<header class="app-bar">
<div class="app-bar__mark"><img src="../assets/mark-photo-64.png" alt=""></div>
<div class="app-bar__title-group">
<div class="app-bar__wordmark">We<span class="v">V</span>isto</div>
<div class="app-bar__sub">— about</div>
</div>
<div class="app-bar__spacer"></div>
<button class="app-bar__icon" aria-label="Close">
<svg viewBox="0 0 24 24"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>
</button>
</header>
<main class="main-scroll">
<div class="about-hero">
<div class="about-hero__plate"><span class="roman"></span><span class="sep"></span><span>Edizione I</span></div>
<div class="about-hero__logo">
<img src="../assets/wordmark.svg" alt="WeVisto">
</div>
<h1 class="about-hero__title">A frame, gifted.</h1>
<p class="about-hero__sub">Handmade e-ink picture frames, given to the people who keep our photographs.</p>
<div class="about-hero__rule"></div>
</div>
<article class="story glass">
<h2><span class="roman">I.</span>The thing itself</h2>
<p>A WeVisto frame is built by hand in <strong>Camogli</strong>, on the Ligurian coast. The display uses paper-like ink — the same family of screens as a Kindle — so it has the <em>quiet of a photograph</em> rather than the glow of a phone.</p>
<p>It connects to your wifi once. After that, nothing. It collects new photographs on its own and changes them as quietly as a clock.</p>
<h2><span class="roman">II.</span>The thing about the V</h2>
<p>The yellow V is the only thing in the wordmark that isn't white. It's how we remember the people we make these for — <em>the recipient is the V,</em> we are the letters around them.</p>
</article>
<div class="credits glass">
<h3>Edition I · MMXXVI</h3>
<dl>
<dt>Made by</dt> <dd>Matt Edholm</dd>
<dt>From</dt> <dd>Camogli, Liguria</dd>
<dt>For</dt> <dd>Margaret, Alice, and the rest</dd>
<dt>Version</dt> <dd>0.4 · pre-release</dd>
</dl>
</div>
<div class="signature">
<div class="signature__mark"><img src="../assets/mark-photo-64.png" alt=""></div>
<div class="signature__text">WeVisto <span class="v-mark">·</span> a frame, gifted</div>
<div class="signature__version">Plate i · v 0.4</div>
</div>
</main>
<nav class="bottom-nav">
<a class="bottom-nav__tab"><svg viewBox="0 0 24 24"><path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/><polyline points="9,22 9,12 15,12 15,22"/></svg><span class="label">Home</span></a>
<a class="bottom-nav__tab"><svg viewBox="0 0 24 24"><rect x="3" y="3" width="7" height="7"/><rect x="14" y="3" width="7" height="7"/><rect x="14" y="14" width="7" height="7"/><rect x="3" y="14" width="7" height="7"/></svg><span class="label">Library</span></a>
<a class="bottom-nav__tab active"><svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="3"/></svg><span class="label">Settings</span></a>
</nav>
</body>
</html>
@@ -0,0 +1,161 @@
<!doctype html>
<html lang="en" data-theme="ocean-dusk">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
<title>Library — WeVisto</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Nunito:wght@400;600;700;900&family=Marcellus&family=Cormorant+Garamond:ital,wght@1,400&family=DM+Mono:wght@400;500&display=swap" rel="stylesheet">
<link rel="stylesheet" href="../spa/_tokens.css">
<link rel="stylesheet" href="../spa/_chrome.css">
<style>
/* This is the library empty state — but the FULL WORDMARK LOGO replaces the 112px mark */
.library-empty {
padding: 48px 24px 0;
display: flex;
flex-direction: column;
align-items: center;
text-align: center;
position: relative;
}
.library-empty__plate {
font: var(--type-label);
letter-spacing: 0.32em;
text-transform: uppercase;
color: var(--text-muted);
margin-bottom: var(--space-5);
display: flex; align-items: center; gap: 10px;
}
.library-empty__plate .roman { color: var(--brand-yellow); font-family: var(--font-display); font-size: var(--text-md); }
.library-empty__plate .sep { width: 24px; height: 1px; background: var(--glass-bord); }
/* THE LOGO MOMENT — the actual wordmark+harbor logo, hero scale */
.library-empty__logo {
width: min(78vw, 320px);
aspect-ratio: 1;
border-radius: 20px;
overflow: hidden;
margin-bottom: var(--space-5);
box-shadow:
0 1px 0 rgba(255,255,255,0.18) inset,
0 24px 48px -12px rgba(0,0,0,0.65),
0 10px 24px -8px rgba(0,0,0,0.4);
position: relative;
}
.library-empty__logo img { width: 100%; height: 100%; display: block; }
/* soft halo behind the logo */
.library-empty__logo::after {
content: '';
position: absolute;
inset: -40px;
background: radial-gradient(circle, color-mix(in srgb, var(--brand-yellow) 22%, transparent), transparent 60%);
border-radius: 50%;
z-index: -1;
pointer-events: none;
}
.library-empty__title {
font: var(--type-display-xl);
color: var(--text);
margin-bottom: var(--space-2);
text-shadow: 0 2px 10px rgba(0,0,0,0.3);
}
.library-empty__title em {
font-family: var(--font-accent);
font-style: italic;
color: var(--brand-yellow);
font-weight: 400;
}
.library-empty__sub {
font: italic 400 18px/1.55 var(--font-accent);
color: var(--text-muted);
max-width: 34ch;
margin-bottom: var(--space-6);
}
.library-empty__rule {
width: 96px; height: 1px;
background: linear-gradient(90deg, transparent, var(--brand-yellow), transparent);
margin: 0 auto var(--space-6);
}
.library-empty__cta {
display: inline-flex;
align-items: center;
gap: 10px;
background: var(--accent);
color: var(--accent-fg);
padding: 14px 28px;
border-radius: var(--radius-full);
font-family: var(--font-family);
font-weight: 700;
font-size: var(--text-base);
text-decoration: none;
box-shadow: 0 8px 22px -4px color-mix(in srgb, var(--accent) 50%, transparent);
transition: transform var(--duration-fast), filter var(--duration-fast);
}
.library-empty__cta:hover { transform: translateY(-1px); filter: brightness(1.08); }
.library-empty__cta svg { width: 18px; height: 18px; stroke: currentColor; fill: none; stroke-width: 2.5; stroke-linecap: round; }
.library-empty__or {
margin-top: var(--space-5);
font: italic 400 var(--text-md)/1.4 var(--font-accent);
color: var(--text-muted);
}
.library-empty__or a {
color: var(--text);
border-bottom: 1px solid var(--glass-bord);
text-decoration: none;
padding-bottom: 1px;
}
.signature { margin: 64px var(--space-5) 0; }
</style>
</head>
<body>
<div class="atmosphere"></div>
<header class="app-bar">
<div class="app-bar__mark"><img src="../assets/mark-photo-64.png" alt=""></div>
<div class="app-bar__title-group">
<div class="app-bar__wordmark">We<span class="v">V</span>isto</div>
<div class="app-bar__sub">— the library</div>
</div>
<div class="app-bar__spacer"></div>
<button class="app-bar__icon" aria-label="Filter">
<svg viewBox="0 0 24 24"><polygon points="22 3 2 3 10 12.46 10 19 14 21 14 12.46 22 3"/></svg>
</button>
</header>
<main class="main-scroll">
<div class="library-empty">
<div class="library-empty__plate"><span class="roman"></span><span class="sep"></span><span>Accession no. 001</span></div>
<div class="library-empty__logo">
<img src="../assets/wordmark.svg" alt="WeVisto">
</div>
<h1 class="library-empty__title">A library, <em>waiting.</em></h1>
<p class="library-empty__sub">Photographs you upload will rotate through Margaret's frame, one at a time.</p>
<div class="library-empty__rule"></div>
<a class="library-empty__cta" href="#">
<svg viewBox="0 0 24 24"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg>
Upload the first photograph
</a>
<div class="library-empty__or">— or <a href="#">invite someone else</a> to send one</div>
</div>
<div class="signature">
<div class="signature__mark"><img src="../assets/mark-photo-64.png" alt=""></div>
<div class="signature__text">WeVisto <span class="v-mark">·</span> a frame, gifted</div>
<div class="signature__version">Edizione I · MMXXVI</div>
</div>
</main>
<nav class="bottom-nav">
<a class="bottom-nav__tab"><svg viewBox="0 0 24 24"><path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/><polyline points="9,22 9,12 15,12 15,22"/></svg><span class="label">Home</span></a>
<a class="bottom-nav__tab active"><svg viewBox="0 0 24 24"><rect x="3" y="3" width="7" height="7"/><rect x="14" y="3" width="7" height="7"/><rect x="14" y="14" width="7" height="7"/><rect x="3" y="14" width="7" height="7"/></svg><span class="label">Library</span></a>
<a class="bottom-nav__tab"><svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="3"/></svg><span class="label">Settings</span></a>
</nav>
</body>
</html>
@@ -0,0 +1,102 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
<title>WeVisto — loading</title>
<style>
:root { --yellow: #f0d000; --cream: #faf3e4; --navy: #07172a; }
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
html, body { width: 100%; height: 100%; overflow: hidden; background: var(--navy); color: var(--cream); }
body { position: relative; display: flex; align-items: center; justify-content: center; }
/* harbor backdrop, Ken-Burns */
.bg {
position: absolute; inset: -4%;
background: url('../assets/harbor.jpg') center/cover no-repeat;
filter: brightness(0.45) saturate(0.85);
opacity: 0;
transform: scale(1.04);
animation:
bd-fade 1.4s 0.1s cubic-bezier(.2,.7,.2,1) forwards,
bd-zoom 26s 0.1s linear forwards;
}
@keyframes bd-fade { to { opacity: 0.5; } }
@keyframes bd-zoom { from { transform: scale(1.04); } to { transform: scale(1.14) translate(-2%, -1%); } }
.vignette {
position: absolute; inset: 0; pointer-events: none;
background:
radial-gradient(ellipse 70% 65% at 50% 50%, transparent 0%, rgba(7,23,42,0.6) 65%, rgba(7,23,42,0.95) 100%);
}
.grain {
position: absolute; inset: 0; pointer-events: none;
background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='320' height='320'><filter id='n'><feTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='2' seed='5'/><feColorMatrix values='0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 0.16 0'/></filter><rect width='320' height='320' filter='url(%23n)'/></svg>");
mix-blend-mode: overlay;
opacity: 0.5;
}
/* THE LOGO — the full wordmark logo at hero scale */
.logo {
position: relative; z-index: 5;
width: min(78vw, 520px);
aspect-ratio: 1;
border-radius: 14px;
overflow: hidden;
box-shadow:
0 1px 0 rgba(255,240,200,0.15) inset,
0 32px 64px -16px rgba(0,0,0,0.7),
0 12px 28px -8px rgba(0,0,0,0.5);
opacity: 0;
transform: translateY(12px) scale(0.98);
animation: logo-in 1.6s 0.4s cubic-bezier(.2,.7,.2,1) forwards;
}
@keyframes logo-in { to { opacity: 1; transform: translateY(0) scale(1); } }
.logo img { width: 100%; height: 100%; display: block; }
/* loader pulse below — dim brand confirmation */
.loader {
position: absolute; bottom: 56px; left: 0; right: 0;
text-align: center;
z-index: 5;
display: flex; align-items: center; justify-content: center; gap: 8px;
opacity: 0;
animation: tag-in 1.2s 1.8s ease forwards;
}
.loader .dot { width: 6px; height: 6px; border-radius: 50%; background: var(--yellow); animation: pulse 1.4s infinite ease-in-out; }
.loader .dot:nth-child(2) { animation-delay: 0.18s; }
.loader .dot:nth-child(3) { animation-delay: 0.36s; }
@keyframes pulse { 0%, 80%, 100% { opacity: 0.25; transform: scale(0.85); } 40% { opacity: 1; transform: scale(1.2); } }
@keyframes tag-in { to { opacity: 0.85; transform: translateY(0); } }
.stamp {
position: absolute; top: 32px; right: 32px;
font-family: Georgia, serif;
font-size: 11px; letter-spacing: 0.46em; color: var(--cream);
text-transform: uppercase;
opacity: 0;
animation: tag-in 1.4s 2.0s ease forwards;
}
.stamp .y { color: var(--yellow); }
@media (prefers-reduced-motion: reduce) {
.bg, .logo, .loader, .stamp { animation: none !important; opacity: 1 !important; transform: none !important; }
.bg { opacity: 0.5; }
}
</style>
</head>
<body>
<div class="bg"></div>
<div class="vignette"></div>
<div class="grain"></div>
<div class="stamp">Edizione I <span class="y">·</span> Plate i</div>
<div class="logo">
<img src="../assets/wordmark.svg" alt="WeVisto">
</div>
<div class="loader"><span class="dot"></span><span class="dot"></span><span class="dot"></span></div>
</body>
</html>
@@ -0,0 +1,183 @@
/* Frosted-glass chrome same vocabulary as the login.
Every surface that holds content is a glass card over the harbor backdrop. */
/* ─── Top app bar — translucent dark band ────────────────────────────── */
.app-bar {
position: sticky;
top: 0;
z-index: 40;
background: var(--glass-2);
backdrop-filter: saturate(180%) blur(18px);
-webkit-backdrop-filter: saturate(180%) blur(18px);
border-bottom: 1px solid var(--glass-bord);
padding: 12px 18px;
padding-top: calc(12px + env(safe-area-inset-top));
display: flex;
align-items: center;
gap: 12px;
}
.app-bar__mark {
width: 34px;
height: 34px;
border-radius: 9px;
overflow: hidden;
box-shadow: 0 2px 6px rgba(0,0,0,0.4);
flex-shrink: 0;
}
.app-bar__mark img { width: 100%; height: 100%; display: block; }
.app-bar__title-group { display: flex; flex-direction: column; }
.app-bar__wordmark {
font: 400 var(--text-lg)/1 var(--font-display);
letter-spacing: 0.005em;
color: var(--text);
}
.app-bar__wordmark .v { color: var(--brand-yellow); }
.app-bar__sub {
font: italic 400 13px/1 var(--font-accent);
color: var(--text-muted);
letter-spacing: 0.02em;
margin-top: 4px;
}
.app-bar__spacer { flex: 1; }
.app-bar__icon {
width: 36px;
height: 36px;
display: flex;
align-items: center;
justify-content: center;
color: var(--text-muted);
background: transparent;
border: 0;
border-radius: var(--radius-full);
cursor: pointer;
transition: background var(--duration-fast) var(--ease-out), color var(--duration-fast) var(--ease-out);
}
.app-bar__icon:hover { background: var(--glass); color: var(--text); }
.app-bar__icon svg { width: 20px; height: 20px; stroke: currentColor; fill: none; stroke-width: 1.8; stroke-linecap: round; stroke-linejoin: round; }
/* ─── Glass surface mixin ──────────────────────────────────────────────── */
.glass {
background: var(--glass);
backdrop-filter: saturate(160%) blur(20px);
-webkit-backdrop-filter: saturate(160%) blur(20px);
border: 1px solid var(--glass-bord);
box-shadow:
0 1px 0 rgba(255,255,255,0.06) inset,
0 20px 40px -16px rgba(0,0,0,0.5);
}
/* ─── Bottom nav — translucent dark band ─────────────────────────────── */
.bottom-nav {
position: fixed;
bottom: 0; left: 0; right: 0;
background: var(--glass-2);
backdrop-filter: saturate(180%) blur(20px);
-webkit-backdrop-filter: saturate(180%) blur(20px);
border-top: 1px solid var(--glass-bord);
display: flex;
z-index: 50;
padding-bottom: env(safe-area-inset-bottom);
}
.bottom-nav__tab {
flex: 1; display: flex; flex-direction: column; align-items: center; justify-content: center;
gap: 2px; height: 64px; color: var(--text-muted);
text-decoration: none; min-height: 44px;
transition: color var(--duration-fast);
}
.bottom-nav__tab.active {
color: var(--brand-yellow);
}
.bottom-nav__tab.active .label { color: var(--text); }
.bottom-nav__tab svg { width: 24px; height: 24px; fill: none; stroke: currentColor; stroke-width: 2; }
.bottom-nav__tab .label {
font-family: var(--font-display);
font-size: var(--text-xs);
letter-spacing: 0.16em;
text-transform: uppercase;
}
.main-scroll {
padding-bottom: calc(64px + env(safe-area-inset-bottom) + 32px);
min-height: 100dvh;
}
/* ─── Brand signature ─────────────────────────────────────────────────── */
.signature {
margin-top: 32px;
padding: 32px 0 16px;
border-top: 1px solid var(--glass-bord);
display: flex;
flex-direction: column;
align-items: center;
gap: 12px;
}
.signature__mark {
width: 44px;
height: 44px;
border-radius: 10px;
overflow: hidden;
opacity: 0.9;
}
.signature__mark img { width: 100%; height: 100%; display: block; }
.signature__text {
font: 400 14px/1.2 var(--font-display);
letter-spacing: 0.32em;
text-transform: uppercase;
color: var(--text-muted);
display: flex;
align-items: center;
gap: 10px;
}
.signature__text .v-mark { color: var(--brand-yellow); }
.signature__version {
font: 400 italic 14px/1.3 var(--font-accent);
letter-spacing: 0.02em;
color: var(--text-muted);
opacity: 0.7;
}
/* ─── Theme switcher (mockup only) ─────────────────────────────────────── */
.theme-switcher {
position: fixed;
bottom: 84px;
right: 16px;
z-index: 60;
background: var(--glass-2);
backdrop-filter: saturate(180%) blur(20px);
border: 1px solid var(--glass-bord);
border-radius: var(--radius-md);
box-shadow: 0 12px 24px -8px rgba(0,0,0,0.5);
padding: 10px;
display: flex;
flex-direction: column;
gap: 8px;
max-width: 220px;
}
.theme-switcher__label {
font: var(--type-label);
letter-spacing: 0.24em;
text-transform: uppercase;
color: var(--text-muted);
}
.theme-switcher__chips {
display: flex;
flex-wrap: wrap;
gap: 4px;
}
.theme-switcher__chip {
width: 26px; height: 26px;
border: 2px solid var(--glass-bord);
border-radius: var(--radius-full);
cursor: pointer;
position: relative;
transition: transform var(--duration-fast);
}
.theme-switcher__chip:hover { transform: scale(1.1); }
.theme-switcher__chip.active { box-shadow: 0 0 0 2px var(--brand-yellow); }
/* each chip shows that theme's accent over its bg-base */
.theme-switcher__chip[data-theme="ocean-dusk"] { background: linear-gradient(135deg, #06121f 50%, #4e9fc8 50%); }
.theme-switcher__chip[data-theme="amber-dusk"] { background: linear-gradient(135deg, #1a0d05 50%, #e89048 50%); }
.theme-switcher__chip[data-theme="sage-dusk"] { background: linear-gradient(135deg, #081208 50%, #88c068 50%); }
.theme-switcher__chip[data-theme="rose-dusk"] { background: linear-gradient(135deg, #1a060f 50%, #d878a0 50%); }
.theme-switcher__chip[data-theme="mauve-dusk"] { background: linear-gradient(135deg, #100618 50%, #b890d8 50%); }
.theme-switcher__chip[data-theme="honey-dusk"] { background: linear-gradient(135deg, #18120a 50%, #e8c050 50%); }
@@ -0,0 +1,19 @@
<div class="theme-switcher" role="region" aria-label="Theme switcher (mockup only)">
<span class="theme-switcher__label">User-pref theme</span>
<div class="theme-switcher__chips">
<button class="theme-switcher__chip active" data-theme="warm-craft" title="warm-craft"></button>
<button class="theme-switcher__chip" data-theme="ocean-dusk" title="ocean-dusk"></button>
<button class="theme-switcher__chip" data-theme="sage-cream" title="sage-cream"></button>
<button class="theme-switcher__chip" data-theme="playful-pop" title="playful-pop"></button>
<button class="theme-switcher__chip" data-theme="dusty-mauve" title="dusty-mauve"></button>
<button class="theme-switcher__chip" data-theme="honey-slate" title="honey-slate"></button>
</div>
</div>
<script>
document.querySelectorAll('.theme-switcher__chip').forEach(chip => {
chip.addEventListener('click', () => {
document.documentElement.setAttribute('data-theme', chip.dataset.theme);
document.querySelectorAll('.theme-switcher__chip').forEach(c => c.classList.toggle('active', c === chip));
});
});
</script>
@@ -0,0 +1,164 @@
/* Atmospheric design tokens themes as dusks layered over the harbor photo.
The whole app shares the login's surface vocabulary. */
:root {
/* Type — same recipe as login */
--font-family: 'Nunito', system-ui, sans-serif;
--font-display: 'Marcellus', Georgia, serif;
--font-accent: 'Cormorant Garamond', Georgia, serif;
--font-mono: 'DM Mono', ui-monospace, monospace;
--text-xs: 11px; --text-sm: 13px; --text-base: 15px; --text-md: 17px;
--text-lg: 20px; --text-xl: 24px; --text-2xl: 28px; --text-3xl: 36px;
--space-1: 4px; --space-2: 8px; --space-3: 12px; --space-4: 16px;
--space-5: 20px; --space-6: 24px; --space-8: 32px;
--radius-sm: 6px; --radius-md: 12px; --radius-lg: 20px; --radius-full: 9999px;
--duration-fast: 150ms; --duration-base: 250ms;
--ease-out: cubic-bezier(0, 0, 0.2, 1);
/* Brand layer — fixed regardless of theme. */
--brand-yellow: #f0d000;
--brand-yellow-soft: #c4a700;
/* Editorial recipes */
--type-display-xl: 400 var(--text-3xl)/1.05 var(--font-display);
--type-display-lg: 400 var(--text-2xl)/1.1 var(--font-display);
--type-display-md: 400 var(--text-xl)/1.15 var(--font-display);
--type-accent-md: 400 italic var(--text-md)/1.4 var(--font-accent);
--type-label: 700 var(--text-xs)/1 var(--font-mono);
}
/* DUSKS
Each dusk is:
--bg-base : the deep base color behind everything
--bg-tint : an rgba multiplied over the harbor photo (the "filter")
--glass : rgba background of cards (frosted, sits on photo)
--glass-2 : a more opaque variant for nav strips
--glass-bord : hairline border on glass surfaces
--accent : theme accent (was --color-primary) CTAs that aren't brand
--accent-fg : text on accent
--text : main text on glass (always light)
--text-muted : muted text on glass
The yellow V brand constant survives across all dusks.
*/
/* OCEAN DUSK — default, closest to login */
[data-theme="ocean-dusk"], :root {
--bg-base: #06121f;
--bg-tint: rgba(8, 22, 38, 0.45);
--glass: rgba(10, 28, 48, 0.55);
--glass-2: rgba(8, 22, 38, 0.72);
--glass-bord: rgba(180, 210, 235, 0.18);
--accent: #4e9fc8;
--accent-fg: #06121f;
--text: #f4eed8;
--text-muted: #b8c8d8;
}
/* AMBER DUSK — successor to warm-craft */
[data-theme="amber-dusk"] {
--bg-base: #1a0d05;
--bg-tint: rgba(60, 25, 8, 0.45);
--glass: rgba(50, 22, 8, 0.55);
--glass-2: rgba(35, 14, 5, 0.72);
--glass-bord: rgba(230, 180, 130, 0.2);
--accent: #e89048;
--accent-fg: #1a0d05;
--text: #faecd0;
--text-muted: #d8b890;
}
/* SAGE DUSK — successor to sage-cream */
[data-theme="sage-dusk"] {
--bg-base: #081208;
--bg-tint: rgba(20, 40, 22, 0.45);
--glass: rgba(18, 38, 22, 0.55);
--glass-2: rgba(12, 26, 14, 0.72);
--glass-bord: rgba(180, 220, 180, 0.18);
--accent: #88c068;
--accent-fg: #081208;
--text: #ecf3e0;
--text-muted: #a8c0a0;
}
/* ROSE DUSK — warmer, classier replacement for playful-pop */
[data-theme="rose-dusk"] {
--bg-base: #1a060f;
--bg-tint: rgba(56, 16, 38, 0.45);
--glass: rgba(48, 14, 36, 0.55);
--glass-2: rgba(32, 8, 22, 0.72);
--glass-bord: rgba(230, 180, 200, 0.2);
--accent: #d878a0;
--accent-fg: #1a060f;
--text: #f8e8ec;
--text-muted: #c898ac;
}
/* MAUVE DUSK — replaces dusty-mauve */
[data-theme="mauve-dusk"] {
--bg-base: #100618;
--bg-tint: rgba(40, 18, 56, 0.45);
--glass: rgba(36, 14, 50, 0.55);
--glass-2: rgba(24, 10, 34, 0.72);
--glass-bord: rgba(210, 190, 230, 0.18);
--accent: #b890d8;
--accent-fg: #100618;
--text: #f0e8f8;
--text-muted: #b8a8c8;
}
/* HONEY DUSK — replaces honey-slate */
[data-theme="honey-dusk"] {
--bg-base: #18120a;
--bg-tint: rgba(48, 36, 14, 0.45);
--glass: rgba(42, 32, 12, 0.55);
--glass-2: rgba(28, 22, 8, 0.72);
--glass-bord: rgba(232, 200, 130, 0.22);
--accent: #e8c050;
--accent-fg: #18120a;
--text: #faf0d8;
--text-muted: #c8b888;
}
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
html, body { background: var(--bg-base); color: var(--text); }
body {
font-family: var(--font-family);
font-size: var(--text-base);
line-height: 1.5;
min-height: 100dvh;
position: relative;
-webkit-font-smoothing: antialiased;
}
/* ─── PERMANENT ATMOSPHERIC BACKDROP ──────────────────────────────────── */
/* harbor photo fixed under everything */
body::before {
content: '';
position: fixed;
inset: 0;
background: url('../assets/harbor.jpg') center/cover no-repeat;
filter: brightness(0.6) saturate(0.85);
z-index: -3;
}
/* theme tint multiplied over photo */
body::after {
content: '';
position: fixed;
inset: 0;
background: var(--bg-tint);
mix-blend-mode: multiply;
z-index: -2;
pointer-events: none;
}
/* global vignette + film grain via one extra layer */
.atmosphere {
position: fixed;
inset: 0;
pointer-events: none;
z-index: -1;
background:
radial-gradient(ellipse 80% 70% at 50% 45%, transparent 0%, rgba(0,0,0,0.35) 70%, rgba(0,0,0,0.65) 100%),
url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='320' height='320'><filter id='n'><feTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='2' seed='5'/><feColorMatrix values='0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 0.1 0'/></filter><rect width='320' height='320' filter='url(%23n)'/></svg>");
}
+310
View File
@@ -0,0 +1,310 @@
<!doctype html>
<html lang="en" data-theme="ocean-dusk">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
<title>Home — WeVisto</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Nunito:wght@400;600;700;900&family=Marcellus&family=Cormorant+Garamond:ital,wght@1,400&family=DM+Mono:wght@400;500&display=swap" rel="stylesheet">
<link rel="stylesheet" href="_tokens.css">
<link rel="stylesheet" href="_chrome.css">
<style>
/* ─── hero — editorial moment, sits directly on the photo ──────────── */
.hero {
padding: var(--space-6) var(--space-5) var(--space-5);
position: relative;
}
.hero__plate {
font: var(--type-label);
letter-spacing: 0.32em;
text-transform: uppercase;
color: var(--text-muted);
margin-bottom: var(--space-3);
display: flex; align-items: center; gap: 10px;
}
.hero__plate .roman { color: var(--brand-yellow); font-family: var(--font-display); font-size: var(--text-md); }
.hero__plate .sep { width: 24px; height: 1px; background: var(--glass-bord); }
.hero__greeting {
font: var(--type-display-lg);
color: var(--text);
margin-bottom: 8px;
}
.hero__greeting em {
font-family: var(--font-accent);
font-style: italic;
color: var(--brand-yellow);
}
.hero__sub {
font: var(--type-accent-md);
color: var(--text-muted);
line-height: 1.5;
max-width: 38ch;
}
.hero__sub strong { color: var(--text); font-style: normal; font-family: var(--font-display); font-weight: 400; }
.hero__rule {
margin-top: var(--space-4);
width: 80px; height: 1px;
background: linear-gradient(90deg, var(--brand-yellow), transparent);
}
/* ─── frame card — frosted glass, the user's photo is the inner content ── */
.frame-card {
margin: var(--space-5);
border-radius: var(--radius-lg);
overflow: hidden;
/* uses .glass class for the frost effect */
}
.frame-card__hero {
aspect-ratio: 4/3;
background-image: url('../assets/harbor.jpg');
background-size: cover;
background-position: center;
position: relative;
/* the user's frame photo — keep at full saturation (it's user content, not brand backdrop) */
}
.frame-card__status {
position: absolute;
top: 14px; left: 14px;
display: inline-flex;
align-items: center;
gap: 6px;
background: rgba(8, 18, 30, 0.7);
backdrop-filter: blur(8px);
padding: 6px 12px;
border-radius: var(--radius-full);
font: var(--type-label);
color: var(--text);
letter-spacing: 0.22em;
text-transform: uppercase;
}
.frame-card__status .dot {
width: 7px; height: 7px;
background: #6cd498;
border-radius: 50%;
box-shadow: 0 0 0 3px rgba(108, 212, 152, 0.25);
}
.frame-card__plate {
position: absolute;
bottom: 14px; left: 14px;
font: var(--type-label);
background: rgba(8, 18, 30, 0.7);
color: var(--text);
padding: 5px 10px;
letter-spacing: 0.28em;
text-transform: uppercase;
backdrop-filter: blur(6px);
}
.frame-card__body { padding: var(--space-5) var(--space-5) var(--space-4); }
.frame-card__name {
font: var(--type-display-md);
color: var(--text);
margin-bottom: 4px;
}
.frame-card__meta {
font: italic 400 15px/1.4 var(--font-accent);
color: var(--text-muted);
margin-bottom: var(--space-4);
}
.frame-card__meta .sep { margin: 0 8px; opacity: 0.4; }
.frame-card__schedule {
background: rgba(255, 255, 255, 0.06);
border: 1px solid var(--glass-bord);
border-radius: var(--radius-md);
padding: var(--space-4);
margin-bottom: var(--space-4);
display: flex;
align-items: flex-start;
gap: var(--space-3);
font-size: var(--text-sm);
line-height: 1.5;
}
.frame-card__schedule svg {
width: 18px; height: 18px;
stroke: var(--brand-yellow);
fill: none;
stroke-width: 2;
flex-shrink: 0;
margin-top: 2px;
}
.frame-card__schedule strong { font-family: var(--font-display); font-size: var(--text-base); font-weight: 400; color: var(--text); display: block; margin-bottom: 2px; }
.frame-card__schedule span { color: var(--text-muted); }
.frame-card__actions { display: flex; gap: var(--space-3); }
.btn {
flex: 1;
padding: 14px 20px;
font-family: var(--font-family);
font-size: var(--text-base);
font-weight: 700;
border-radius: var(--radius-md);
border: 1px solid var(--glass-bord);
background: rgba(255, 255, 255, 0.06);
color: var(--text);
cursor: pointer;
transition: background var(--duration-fast);
display: inline-flex; align-items: center; justify-content: center; gap: 8px;
}
.btn:hover { background: rgba(255, 255, 255, 0.12); }
.btn--primary {
background: var(--accent);
color: var(--accent-fg);
border-color: var(--accent);
font-weight: 700;
}
.btn--primary:hover { filter: brightness(1.08); }
.btn svg { width: 16px; height: 16px; stroke: currentColor; fill: none; stroke-width: 2; }
/* ─── upcoming photos strip — quiet teaser of what's queued ─────────── */
.upcoming {
margin: var(--space-5);
padding: var(--space-5);
border-radius: var(--radius-lg);
}
.upcoming__head {
display: flex; align-items: baseline; justify-content: space-between;
margin-bottom: var(--space-4);
}
.upcoming__title {
font: var(--type-display-md);
color: var(--text);
}
.upcoming__hint {
font: var(--type-accent-md);
color: var(--text-muted);
}
.upcoming__row {
display: flex; gap: var(--space-3); overflow-x: auto; padding-bottom: 4px;
}
.upcoming__row::-webkit-scrollbar { display: none; }
.upcoming__row { scrollbar-width: none; }
.thumb {
flex-shrink: 0;
width: 92px; height: 92px;
border-radius: var(--radius-md);
overflow: hidden;
background: rgba(0,0,0,0.3);
position: relative;
border: 1px solid var(--glass-bord);
}
.thumb img { width: 100%; height: 100%; object-fit: cover; display: block; }
.thumb .when {
position: absolute;
bottom: 4px; left: 4px;
font: var(--type-label);
background: rgba(0,0,0,0.7);
color: var(--text);
padding: 2px 6px;
letter-spacing: 0.18em;
}
</style>
</head>
<body>
<div class="atmosphere"></div>
<header class="app-bar">
<div class="app-bar__mark"><img src="../assets/mark-photo-64.png" alt=""></div>
<div class="app-bar__title-group">
<div class="app-bar__wordmark">We<span class="v">V</span>isto</div>
<div class="app-bar__sub">— Margaret's frame</div>
</div>
<div class="app-bar__spacer"></div>
<button class="app-bar__icon" aria-label="Notifications">
<svg viewBox="0 0 24 24"><path d="M18 8a6 6 0 0 0-12 0c0 7-3 9-3 9h18s-3-2-3-9"/><path d="M13.73 21a2 2 0 0 1-3.46 0"/></svg>
</button>
</header>
<main class="main-scroll">
<section class="hero">
<div class="hero__plate"><span class="roman">I.</span><span class="sep"></span><span>Plate · today</span></div>
<h1 class="hero__greeting">Good morning, <em>Alice.</em></h1>
<p class="hero__sub">Margaret's frame is in sync. The next photograph will arrive at <strong>7:00 AM</strong>, quietly, on its own.</p>
<div class="hero__rule"></div>
</section>
<article class="frame-card glass">
<div class="frame-card__hero">
<div class="frame-card__status">
<span class="dot"></span><span>Synced 12 min ago</span>
</div>
<div class="frame-card__plate">Plate XII</div>
</div>
<div class="frame-card__body">
<h2 class="frame-card__name">Margaret's frame</h2>
<p class="frame-card__meta">7.3″ landscape <span class="sep">·</span> Camden, Maine <span class="sep">·</span> est. May 2026</p>
<div class="frame-card__schedule">
<svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></svg>
<div>
<strong>Next photograph at 7:00 AM</strong>
<span>then again at 12:00 PM and 6:00 PM</span>
</div>
</div>
<div class="frame-card__actions">
<button class="btn btn--primary">
<svg viewBox="0 0 24 24"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg>
Add photograph
</button>
<button class="btn">
<svg viewBox="0 0 24 24"><path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/><path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/></svg>
Settings
</button>
</div>
</div>
</article>
<section class="upcoming glass">
<div class="upcoming__head">
<h3 class="upcoming__title">Up next</h3>
<span class="upcoming__hint">— in the library queue</span>
</div>
<div class="upcoming__row">
<div class="thumb"><img src="../assets/harbor.jpg" alt=""><span class="when">7am</span></div>
<div class="thumb"><img src="../assets/harbor.jpg" alt="" style="filter: hue-rotate(40deg);"><span class="when">noon</span></div>
<div class="thumb"><img src="../assets/harbor.jpg" alt="" style="filter: hue-rotate(-30deg) saturate(1.2);"><span class="when">6pm</span></div>
<div class="thumb"><img src="../assets/harbor.jpg" alt="" style="filter: brightness(0.85);"><span class="when">tmrw</span></div>
<div class="thumb"><img src="../assets/harbor.jpg" alt="" style="filter: sepia(0.4);"><span class="when">+5</span></div>
</div>
</section>
</main>
<nav class="bottom-nav">
<a class="bottom-nav__tab active" href="home.html">
<svg viewBox="0 0 24 24"><path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/><polyline points="9,22 9,12 15,12 15,22"/></svg>
<span class="label">Home</span>
</a>
<a class="bottom-nav__tab" href="library.html">
<svg viewBox="0 0 24 24"><rect x="3" y="3" width="7" height="7"/><rect x="14" y="3" width="7" height="7"/><rect x="14" y="14" width="7" height="7"/><rect x="3" y="14" width="7" height="7"/></svg>
<span class="label">Library</span>
</a>
<a class="bottom-nav__tab" href="settings.html">
<svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 1 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 1 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 1 1-2.83-2.83l.06-.06A1.65 1.65 0 0 0 4.68 15a1.65 1.65 0 0 0-1.51-1H3a2 2 0 1 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 1 1 2.83-2.83l.06.06A1.65 1.65 0 0 0 9 4.68a1.65 1.65 0 0 0 1-1.51V3a2 2 0 1 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 1 1 2.83 2.83l-.06.06A1.65 1.65 0 0 0 19.4 9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 1 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z"/></svg>
<span class="label">Settings</span>
</a>
</nav>
<div class="theme-switcher" role="region" aria-label="Theme switcher (mockup only)">
<span class="theme-switcher__label">Dusk</span>
<div class="theme-switcher__chips">
<button class="theme-switcher__chip active" data-theme="ocean-dusk" title="Ocean dusk"></button>
<button class="theme-switcher__chip" data-theme="amber-dusk" title="Amber dusk"></button>
<button class="theme-switcher__chip" data-theme="sage-dusk" title="Sage dusk"></button>
<button class="theme-switcher__chip" data-theme="rose-dusk" title="Rose dusk"></button>
<button class="theme-switcher__chip" data-theme="mauve-dusk" title="Mauve dusk"></button>
<button class="theme-switcher__chip" data-theme="honey-dusk" title="Honey dusk"></button>
</div>
</div>
<script>
document.querySelectorAll('.theme-switcher__chip').forEach(chip => {
chip.addEventListener('click', () => {
document.documentElement.setAttribute('data-theme', chip.dataset.theme);
document.querySelectorAll('.theme-switcher__chip').forEach(c => c.classList.toggle('active', c === chip));
});
});
</script>
</body>
</html>
@@ -0,0 +1,249 @@
<!doctype html>
<html lang="en" data-theme="ocean-dusk">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
<title>Library — WeVisto</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Nunito:wght@400;600;700;900&family=Marcellus&family=Cormorant+Garamond:ital,wght@1,400&family=DM+Mono:wght@400;500&display=swap" rel="stylesheet">
<link rel="stylesheet" href="_tokens.css">
<link rel="stylesheet" href="_chrome.css">
<style>
.library-empty {
padding: 56px 24px 0;
display: flex;
flex-direction: column;
align-items: center;
text-align: center;
position: relative;
}
.library-empty__plate {
font: var(--type-label);
letter-spacing: 0.32em;
text-transform: uppercase;
color: var(--text-muted);
margin-bottom: var(--space-5);
display: flex; align-items: center; gap: 10px;
}
.library-empty__plate .roman { color: var(--brand-yellow); font-family: var(--font-display); font-size: var(--text-md); }
.library-empty__plate .sep { width: 24px; height: 1px; background: var(--glass-bord); }
.library-empty__halo { position: relative; margin-bottom: var(--space-5); }
.library-empty__halo::before {
content: '';
position: absolute;
top: 50%; left: 50%;
width: 240px; height: 240px;
transform: translate(-50%, -50%);
background: radial-gradient(circle, color-mix(in srgb, var(--brand-yellow) 25%, transparent), transparent 65%);
border-radius: 50%;
pointer-events: none;
z-index: -1;
}
.library-empty__mark {
width: 112px;
height: 112px;
border-radius: 24px;
overflow: hidden;
box-shadow:
0 1px 0 rgba(255,255,255,0.18) inset,
0 20px 40px -10px rgba(0,0,0,0.6),
0 8px 20px -6px rgba(0,0,0,0.4);
}
.library-empty__mark img { width: 100%; height: 100%; display: block; }
.library-empty__title {
font: var(--type-display-xl);
color: var(--text);
margin-bottom: var(--space-2);
text-shadow: 0 2px 10px rgba(0,0,0,0.3);
}
.library-empty__title em {
font-family: var(--font-accent);
font-style: italic;
color: var(--brand-yellow);
font-weight: 400;
}
.library-empty__sub {
font: italic 400 18px/1.55 var(--font-accent);
color: var(--text-muted);
max-width: 36ch;
margin-bottom: var(--space-6);
}
.library-empty__rule {
width: 96px; height: 1px;
background: linear-gradient(90deg, transparent, var(--brand-yellow), transparent);
margin: 0 auto var(--space-6);
}
.library-empty__cta {
display: inline-flex;
align-items: center;
gap: 10px;
background: var(--accent);
color: var(--accent-fg);
padding: 14px 28px;
border-radius: var(--radius-full);
font-family: var(--font-family);
font-weight: 700;
font-size: var(--text-base);
text-decoration: none;
box-shadow: 0 8px 22px -4px color-mix(in srgb, var(--accent) 50%, transparent);
transition: transform var(--duration-fast) var(--ease-out), filter var(--duration-fast) var(--ease-out);
}
.library-empty__cta:hover { transform: translateY(-1px); filter: brightness(1.08); }
.library-empty__cta svg { width: 18px; height: 18px; stroke: currentColor; fill: none; stroke-width: 2.5; stroke-linecap: round; }
.library-empty__or {
margin-top: var(--space-5);
font: italic 400 var(--text-md)/1.4 var(--font-accent);
color: var(--text-muted);
}
.library-empty__or a {
color: var(--text);
border-bottom: 1px solid var(--glass-bord);
text-decoration: none;
padding-bottom: 1px;
}
/* steps card — frosted */
.intro-steps {
margin: 48px var(--space-5) 0;
padding: var(--space-5) var(--space-5) var(--space-6);
border-radius: var(--radius-lg);
position: relative;
}
.intro-steps__rule {
position: absolute;
top: -1px; left: 50%;
transform: translateX(-50%);
width: 60px; height: 1px;
background: var(--brand-yellow);
}
.intro-steps__title {
font: var(--type-display-md);
color: var(--text);
text-align: center;
margin-bottom: var(--space-2);
}
.intro-steps__title .v-mark { color: var(--brand-yellow); }
.intro-steps__sub {
font: var(--type-accent-md);
color: var(--text-muted);
text-align: center;
margin-bottom: var(--space-5);
}
.intro-steps__list { display: flex; gap: var(--space-3); }
.step { flex: 1; text-align: center; padding: var(--space-3); }
.step__num {
font-family: var(--font-display);
font-size: var(--text-2xl);
color: var(--brand-yellow);
line-height: 1;
margin-bottom: var(--space-2);
}
.step__label {
font-size: var(--text-xs);
color: var(--text-muted);
line-height: 1.4;
}
.step__label strong {
font-family: var(--font-display);
font-weight: 400;
font-size: var(--text-sm);
color: var(--text);
display: block;
margin-bottom: 2px;
}
.signature { margin: 64px var(--space-5) 0; }
</style>
</head>
<body>
<div class="atmosphere"></div>
<header class="app-bar">
<div class="app-bar__mark"><img src="../assets/mark-photo-64.png" alt=""></div>
<div class="app-bar__title-group">
<div class="app-bar__wordmark">We<span class="v">V</span>isto</div>
<div class="app-bar__sub">— the library</div>
</div>
<div class="app-bar__spacer"></div>
<button class="app-bar__icon" aria-label="Filter">
<svg viewBox="0 0 24 24"><polygon points="22 3 2 3 10 12.46 10 19 14 21 14 12.46 22 3"/></svg>
</button>
</header>
<main class="main-scroll">
<div class="library-empty">
<div class="library-empty__plate"><span class="roman"></span><span class="sep"></span><span>Accession no. 001</span></div>
<div class="library-empty__halo">
<div class="library-empty__mark"><img src="../assets/mark-photo.png" alt=""></div>
</div>
<h1 class="library-empty__title">A library, <em>waiting.</em></h1>
<p class="library-empty__sub">Photographs you upload will rotate through Margaret's frame, one at a time. We'll hold them here until the frame is ready.</p>
<div class="library-empty__rule"></div>
<a class="library-empty__cta" href="#">
<svg viewBox="0 0 24 24"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg>
Upload the first photograph
</a>
<div class="library-empty__or">— or <a href="#">invite someone else</a> to send one</div>
</div>
<div class="intro-steps glass">
<div class="intro-steps__rule"></div>
<div class="intro-steps__title">How <span class="v-mark">V</span>isto works, briefly</div>
<div class="intro-steps__sub">— three steps, then forever</div>
<div class="intro-steps__list">
<div class="step"><div class="step__num">I.</div><div class="step__label"><strong>You upload</strong>family photos</div></div>
<div class="step"><div class="step__num">II.</div><div class="step__label"><strong>We hold</strong>them in the library</div></div>
<div class="step"><div class="step__num">III.</div><div class="step__label"><strong>Frame picks</strong>one each interval</div></div>
</div>
</div>
<div class="signature">
<div class="signature__mark"><img src="../assets/mark-photo-64.png" alt=""></div>
<div class="signature__text">WeVisto <span class="v-mark">·</span> a frame, gifted</div>
<div class="signature__version">Edizione I · MMXXVI</div>
</div>
</main>
<nav class="bottom-nav">
<a class="bottom-nav__tab" href="home.html">
<svg viewBox="0 0 24 24"><path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/><polyline points="9,22 9,12 15,12 15,22"/></svg>
<span class="label">Home</span>
</a>
<a class="bottom-nav__tab active" href="library.html">
<svg viewBox="0 0 24 24"><rect x="3" y="3" width="7" height="7"/><rect x="14" y="3" width="7" height="7"/><rect x="14" y="14" width="7" height="7"/><rect x="3" y="14" width="7" height="7"/></svg>
<span class="label">Library</span>
</a>
<a class="bottom-nav__tab" href="settings.html">
<svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 1 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 1 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 1 1-2.83-2.83l.06-.06A1.65 1.65 0 0 0 4.68 15a1.65 1.65 0 0 0-1.51-1H3a2 2 0 1 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 1 1 2.83-2.83l.06.06A1.65 1.65 0 0 0 9 4.68a1.65 1.65 0 0 0 1-1.51V3a2 2 0 1 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 1 1 2.83 2.83l-.06.06A1.65 1.65 0 0 0 19.4 9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 1 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z"/></svg>
<span class="label">Settings</span>
</a>
</nav>
<div class="theme-switcher" role="region" aria-label="Theme switcher (mockup only)">
<span class="theme-switcher__label">Dusk</span>
<div class="theme-switcher__chips">
<button class="theme-switcher__chip active" data-theme="ocean-dusk" title="Ocean dusk"></button>
<button class="theme-switcher__chip" data-theme="amber-dusk" title="Amber dusk"></button>
<button class="theme-switcher__chip" data-theme="sage-dusk" title="Sage dusk"></button>
<button class="theme-switcher__chip" data-theme="rose-dusk" title="Rose dusk"></button>
<button class="theme-switcher__chip" data-theme="mauve-dusk" title="Mauve dusk"></button>
<button class="theme-switcher__chip" data-theme="honey-dusk" title="Honey dusk"></button>
</div>
</div>
<script>
document.querySelectorAll('.theme-switcher__chip').forEach(chip => {
chip.addEventListener('click', () => {
document.documentElement.setAttribute('data-theme', chip.dataset.theme);
document.querySelectorAll('.theme-switcher__chip').forEach(c => c.classList.toggle('active', c === chip));
});
});
</script>
</body>
</html>
@@ -0,0 +1,349 @@
<!doctype html>
<html lang="en" data-theme="ocean-dusk">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
<title>Settings — WeVisto</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Nunito:wght@400;600;700;900&family=Marcellus&family=Cormorant+Garamond:ital,wght@1,400&family=DM+Mono:wght@400;500&display=swap" rel="stylesheet">
<link rel="stylesheet" href="_tokens.css">
<link rel="stylesheet" href="_chrome.css">
<style>
.page-title {
padding: var(--space-6) var(--space-5) var(--space-3);
text-align: center;
}
.page-title__plate {
font: var(--type-label);
letter-spacing: 0.32em;
text-transform: uppercase;
color: var(--text-muted);
margin-bottom: var(--space-2);
}
.page-title__plate .v-mark { color: var(--brand-yellow); font-family: var(--font-display); font-size: var(--text-md); margin: 0 8px; }
.page-title__title {
font: var(--type-display-lg);
color: var(--text);
text-shadow: 0 2px 10px rgba(0,0,0,0.3);
}
.page-title__rule {
width: 60px; height: 1px;
margin: var(--space-3) auto 0;
background: linear-gradient(90deg, transparent, var(--brand-yellow), transparent);
}
.section { margin: var(--space-6) var(--space-5) 0; }
.section__head {
display: flex;
align-items: baseline;
gap: 12px;
margin-bottom: var(--space-3);
padding: 0 var(--space-2);
}
.section__roman {
font-family: var(--font-display);
font-size: var(--text-lg);
color: var(--brand-yellow);
line-height: 1;
}
.section__label {
font: var(--type-label);
letter-spacing: 0.32em;
text-transform: uppercase;
color: var(--text-muted);
}
.section__rule { flex: 1; height: 1px; background: var(--glass-bord); }
.list {
border-radius: var(--radius-lg);
overflow: hidden;
}
.row {
display: flex;
align-items: center;
gap: var(--space-3);
padding: var(--space-4) var(--space-5);
border-bottom: 1px solid var(--glass-bord);
text-decoration: none;
color: inherit;
cursor: pointer;
transition: background var(--duration-fast);
}
.row:last-child { border-bottom: 0; }
.row:hover { background: rgba(255,255,255,0.05); }
.row__icon { width: 22px; height: 22px; flex-shrink: 0; color: var(--text-muted); }
.row__icon svg { width: 100%; height: 100%; stroke: currentColor; fill: none; stroke-width: 2; }
.row__label {
flex: 1;
font-family: var(--font-family);
font-size: var(--text-base);
font-weight: 700;
color: var(--text);
}
.row__value {
font: italic 400 14px/1.3 var(--font-accent);
color: var(--text-muted);
}
.row__chevron { color: var(--text-muted); opacity: 0.6; }
/* DUSK PICKER */
.theme-picker {
border-radius: var(--radius-lg);
padding: var(--space-5);
}
.theme-picker__head {
display: flex;
align-items: baseline;
justify-content: space-between;
margin-bottom: var(--space-4);
}
.theme-picker__title {
font: var(--type-display-md);
color: var(--text);
}
.theme-picker__hint {
font: var(--type-accent-md);
color: var(--text-muted);
}
.theme-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: var(--space-3); }
.theme-swatch {
border-radius: var(--radius-md);
padding: var(--space-3);
border: 1px solid var(--glass-bord);
background: rgba(0,0,0,0.25);
cursor: pointer;
text-align: left;
transition: transform var(--duration-fast), box-shadow var(--duration-fast);
font-family: inherit;
color: var(--text);
position: relative;
overflow: hidden;
}
.theme-swatch.active {
border-color: var(--brand-yellow);
box-shadow: 0 0 0 2px color-mix(in srgb, var(--brand-yellow) 35%, transparent);
}
.theme-swatch:hover { transform: translateY(-2px); }
/* a tinted preview of harbor inside each swatch */
.theme-swatch__preview {
aspect-ratio: 3/2;
border-radius: var(--radius-sm);
overflow: hidden;
margin-bottom: var(--space-2);
background-image: url('../assets/harbor.jpg');
background-size: cover;
background-position: center;
position: relative;
}
.theme-swatch__preview::after {
content: '';
position: absolute;
inset: 0;
mix-blend-mode: multiply;
}
.theme-swatch[data-pref="ocean-dusk"] .theme-swatch__preview::after { background: rgba(8, 22, 38, 0.55); }
.theme-swatch[data-pref="amber-dusk"] .theme-swatch__preview::after { background: rgba(60, 25, 8, 0.55); }
.theme-swatch[data-pref="sage-dusk"] .theme-swatch__preview::after { background: rgba(20, 40, 22, 0.55); }
.theme-swatch[data-pref="rose-dusk"] .theme-swatch__preview::after { background: rgba(56, 16, 38, 0.55); }
.theme-swatch[data-pref="mauve-dusk"] .theme-swatch__preview::after { background: rgba(40, 18, 56, 0.55); }
.theme-swatch[data-pref="honey-dusk"] .theme-swatch__preview::after { background: rgba(48, 36, 14, 0.55); }
.theme-swatch__name {
font-family: var(--font-display);
font-size: 13px;
color: var(--text);
letter-spacing: 0.04em;
}
.theme-swatch__italic {
font: italic 400 12px/1.2 var(--font-accent);
color: var(--text-muted);
margin-top: 2px;
display: block;
}
.sign-out { margin: var(--space-6) var(--space-5); text-align: center; }
.sign-out a {
color: #e08070;
font-family: var(--font-family);
font-weight: 700;
text-decoration: none;
font-size: var(--text-base);
padding: var(--space-4) var(--space-5);
display: inline-block;
}
.sign-out a:hover { text-decoration: underline; }
.signature { margin: 0 var(--space-5); }
</style>
</head>
<body>
<div class="atmosphere"></div>
<header class="app-bar">
<div class="app-bar__mark"><img src="../assets/mark-photo-64.png" alt=""></div>
<div class="app-bar__title-group">
<div class="app-bar__wordmark">We<span class="v">V</span>isto</div>
<div class="app-bar__sub">— settings</div>
</div>
<div class="app-bar__spacer"></div>
</header>
<main class="main-scroll">
<div class="page-title">
<div class="page-title__plate">Settings <span class="v-mark">·</span> Plate i</div>
<h1 class="page-title__title">A few quiet choices.</h1>
<div class="page-title__rule"></div>
</div>
<div class="section">
<div class="section__head"><span class="section__roman">I.</span><span class="section__label">Account</span><span class="section__rule"></span></div>
<div class="list glass">
<a class="row">
<span class="row__icon"><svg viewBox="0 0 24 24"><circle cx="12" cy="8" r="4"/><path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/></svg></span>
<span class="row__label">Alice Wexler</span>
<span class="row__value">alice@example.com</span>
<span class="row__chevron"></span>
</a>
<a class="row">
<span class="row__icon"><svg viewBox="0 0 24 24"><path d="M4 4h16c1.1 0 2 .9 2 2v12c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V6c0-1.1.9-2 2-2z"/><polyline points="22,6 12,13 2,6"/></svg></span>
<span class="row__label">Notification email</span>
<span class="row__value">daily digest</span>
<span class="row__chevron"></span>
</a>
</div>
</div>
<div class="section">
<div class="section__head"><span class="section__roman">II.</span><span class="section__label">Appearance</span><span class="section__rule"></span></div>
<div class="theme-picker glass">
<div class="theme-picker__head">
<div class="theme-picker__title">Dusk</div>
<div class="theme-picker__hint">— pick a tint for the room</div>
</div>
<div class="theme-grid">
<button class="theme-swatch active" data-pref="ocean-dusk">
<div class="theme-swatch__preview"></div>
<div class="theme-swatch__name">Ocean dusk</div>
<span class="theme-swatch__italic">— the harbor</span>
</button>
<button class="theme-swatch" data-pref="amber-dusk">
<div class="theme-swatch__preview"></div>
<div class="theme-swatch__name">Amber dusk</div>
<span class="theme-swatch__italic">— the workshop</span>
</button>
<button class="theme-swatch" data-pref="sage-dusk">
<div class="theme-swatch__preview"></div>
<div class="theme-swatch__name">Sage dusk</div>
<span class="theme-swatch__italic">— the garden</span>
</button>
<button class="theme-swatch" data-pref="rose-dusk">
<div class="theme-swatch__preview"></div>
<div class="theme-swatch__name">Rose dusk</div>
<span class="theme-swatch__italic">— the parlor</span>
</button>
<button class="theme-swatch" data-pref="mauve-dusk">
<div class="theme-swatch__preview"></div>
<div class="theme-swatch__name">Mauve dusk</div>
<span class="theme-swatch__italic">— the study</span>
</button>
<button class="theme-swatch" data-pref="honey-dusk">
<div class="theme-swatch__preview"></div>
<div class="theme-swatch__name">Honey dusk</div>
<span class="theme-swatch__italic">— the alcove</span>
</button>
</div>
</div>
</div>
<div class="section">
<div class="section__head"><span class="section__roman">III.</span><span class="section__label">Frames</span><span class="section__rule"></span></div>
<div class="list glass">
<a class="row">
<span class="row__icon"><svg viewBox="0 0 24 24"><rect x="3" y="3" width="18" height="18" rx="2"/><circle cx="8.5" cy="8.5" r="1.5"/><polyline points="21,15 16,10 5,21"/></svg></span>
<span class="row__label">Margaret's frame</span>
<span class="row__value">7.3″ landscape</span>
<span class="row__chevron"></span>
</a>
<a class="row">
<span class="row__icon"><svg viewBox="0 0 24 24"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg></span>
<span class="row__label">Add another frame</span>
<span class="row__chevron"></span>
</a>
</div>
</div>
<div class="section">
<div class="section__head"><span class="section__roman">IV.</span><span class="section__label">Help</span><span class="section__rule"></span></div>
<div class="list glass">
<a class="row">
<span class="row__icon"><svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="10"/><path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg></span>
<span class="row__label">How WeVisto works</span>
<span class="row__chevron"></span>
</a>
<a class="row">
<span class="row__icon"><svg viewBox="0 0 24 24"><path d="M21 11.5a8.38 8.38 0 0 1-.9 3.8 8.5 8.5 0 0 1-7.6 4.7 8.38 8.38 0 0 1-3.8-.9L3 21l1.9-5.7a8.38 8.38 0 0 1-.9-3.8 8.5 8.5 0 0 1 4.7-7.6 8.38 8.38 0 0 1 3.8-.9h.5a8.48 8.48 0 0 1 8 8v.5z"/></svg></span>
<span class="row__label">Contact us</span>
<span class="row__chevron"></span>
</a>
</div>
</div>
<div class="sign-out"><a href="#">Sign out</a></div>
<div class="signature">
<div class="signature__mark"><img src="../assets/mark-photo-64.png" alt=""></div>
<div class="signature__text">WeVisto <span class="v-mark">·</span> a frame, gifted</div>
<div class="signature__version">Edizione I · MMXXVI · Camogli</div>
</div>
</main>
<nav class="bottom-nav">
<a class="bottom-nav__tab" href="home.html">
<svg viewBox="0 0 24 24"><path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/><polyline points="9,22 9,12 15,12 15,22"/></svg>
<span class="label">Home</span>
</a>
<a class="bottom-nav__tab" href="library.html">
<svg viewBox="0 0 24 24"><rect x="3" y="3" width="7" height="7"/><rect x="14" y="3" width="7" height="7"/><rect x="14" y="14" width="7" height="7"/><rect x="3" y="14" width="7" height="7"/></svg>
<span class="label">Library</span>
</a>
<a class="bottom-nav__tab active" href="settings.html">
<svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 1 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 1 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 1 1-2.83-2.83l.06-.06A1.65 1.65 0 0 0 4.68 15a1.65 1.65 0 0 0-1.51-1H3a2 2 0 1 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 1 1 2.83-2.83l.06.06A1.65 1.65 0 0 0 9 4.68a1.65 1.65 0 0 0 1-1.51V3a2 2 0 1 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 1 1 2.83 2.83l-.06.06A1.65 1.65 0 0 0 19.4 9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 1 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z"/></svg>
<span class="label">Settings</span>
</a>
</nav>
<div class="theme-switcher" role="region" aria-label="Theme switcher (mockup only)">
<span class="theme-switcher__label">Dusk</span>
<div class="theme-switcher__chips">
<button class="theme-switcher__chip active" data-theme="ocean-dusk" title="Ocean dusk"></button>
<button class="theme-switcher__chip" data-theme="amber-dusk" title="Amber dusk"></button>
<button class="theme-switcher__chip" data-theme="sage-dusk" title="Sage dusk"></button>
<button class="theme-switcher__chip" data-theme="rose-dusk" title="Rose dusk"></button>
<button class="theme-switcher__chip" data-theme="mauve-dusk" title="Mauve dusk"></button>
<button class="theme-switcher__chip" data-theme="honey-dusk" title="Honey dusk"></button>
</div>
</div>
<script>
document.querySelectorAll('.theme-switcher__chip').forEach(chip => {
chip.addEventListener('click', () => {
const t = chip.dataset.theme;
document.documentElement.setAttribute('data-theme', t);
document.querySelectorAll('.theme-switcher__chip').forEach(c => c.classList.toggle('active', c === chip));
document.querySelectorAll('.theme-swatch').forEach(s => s.classList.toggle('active', s.dataset.pref === t));
});
});
document.querySelectorAll('.theme-swatch').forEach(sw => {
sw.addEventListener('click', () => {
const t = sw.dataset.pref;
document.documentElement.setAttribute('data-theme', t);
document.querySelectorAll('.theme-swatch').forEach(s => s.classList.toggle('active', s === sw));
document.querySelectorAll('.theme-switcher__chip').forEach(c => c.classList.toggle('active', c.dataset.theme === t));
});
});
</script>
</body>
</html>