63 Commits

Author SHA1 Message Date
football2801 b355572a78 fix(render): V2 panel ribbon-at-bottom mounting compensation
CI / test (push) Has been cancelled
DeviceModel gains physicalRotationDegrees() — extra rotation applied
after the orientation-fit step, compensating for the panel's physical
mounting vs its scan-zero corner. V2 (13.3" Spectra-6) is mounted with
the ribbon at the bottom of a portrait photo (opposite scan-zero), so
needs 180°. V1 → 0.

Existing rendered .bin files will need a re-render to take effect —
run 'app:rerender-assets' on prod after deploy.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-17 13:14:06 -04:00
football2801 019a3363c5 feat(design): v2 becomes the default — drop beta conversation
CI / test (push) Has been cancelled
Atmospheric design is now the default for everyone:
- User.getDesignVersion() returns 'v2' when unset (was 'v1')
- All Twig templates default the cookie read to 'v2'
- SettingsView 'Design (beta)' section removed entirely along with the
  selectDesign() handler and currentDesign computed

The /api/user/design endpoint stays in place so the design_version column
can still be set programmatically (or migrated later), but the UI no
longer exposes it as a user-facing choice. Existing users with explicit
'v1' in their DB row continue to see v1 — their preference persists.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 20:22:18 -04:00
football2801 c794878e5e polish(design-v2): Manage button bar more obviously button-like
CI / test (push) Has been cancelled
Min-height 44px (touch target), 'Manage ▸' wrapped in a yellow-bordered
chip with subtle yellow background, whole bar hovers to a brighter
yellow tint. Reads as a tap target now, not just text.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 17:07:54 -04:00
football2801 99b3fb9e59 polish(design-v2): library tile pops above harbor backdrop
CI / test (push) Has been cancelled
Surface was --color-surface (same tone as the multiplied backdrop) so
the card blended in. Switched to --color-surface-2 (lighter shade) +
stronger shadow + inset highlight so the tile clearly floats above
the harbor.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 17:06:24 -04:00
football2801 25f698f067 polish(design-v2): library tile is one connected card, Manage feels like a button
CI / test (push) Has been cancelled
Each library tile becomes a single glass card containing the photo and
the Manage row with no gap — the Manage button bar sits below the photo
with its own dark-glass background and a 1px top divider so it reads as
a tappable footer. Manage-action ('Manage ▸') in yellow for affordance.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 17:04:27 -04:00
football2801 59f5e6e0eb polish(design-v2): settings reorder + section cards + cog + yellow active
CI / test (push) Has been cancelled
User-reported gaps:
- Settings section order: Account top, Theme middle, Design bottom,
  Install at end (was Install/Design/Theme/Account)
- Settings sections wrapped in glass cards with backdrop blur so they
  read against the harbor backdrop (was flat text)
- Desktop settings now 2-column grid; section cards pack in pairs
- FrameCard cog button: was white-opaque, now dark glass with light
  icon so it sits cleanly on the v2 dark photo backdrop
- Orientation picker active state: yellow border + yellow diagram tint
  (was theme primary which clashed with the dusks aesthetic)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 16:51:03 -04:00
football2801 1320b1e4b2 fix(brand): yellow V in TopNav wordmark
CI / test (push) Has been cancelled
Wordmark was plain text 'WeVisto'; the V should be yellow #f0d000
(the brand accent) — consistent with the logo.svg and every other
'WeVisto' rendering in the design.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 16:40:58 -04:00
football2801 d554951077 fix(design-v2): !important on top-nav__tabs hide at mobile/tablet
CI / test (push) Has been cancelled
2026-05-15 15:59:40 -04:00
football2801 f27c00b965 fix(design-v2): show top nav at mobile/tablet (slim brand bar)
CI / test (push) Has been cancelled
v1 hides TopNav below 960px; v2 was inheriting that, so mobile + tablet
had zero header chrome — frame card sat at viewport top, no WeVisto
wordmark, no page subtitle. Visible gap from the mockup.

Now in v2:
- Mobile/tablet: TopNav renders as a 52px slim sticky top bar with
  mark + WeVisto wordmark + italic '— home/library/settings' subtitle.
  Nav tabs are hidden (BottomNav handles routing at small sizes).
- Desktop: TopNav becomes the left side rail with full vertical nav.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 15:58:08 -04:00
football2801 c0f6ba2651 fix(design-v2): make body bg transparent so harbor pseudo shows through
CI / test (push) Has been cancelled
body had opaque background: var(--color-bg) which sat at z-index 0 and
covered the body::before harbor at z-index -3. Switched body to
transparent and put the fallback color on html instead.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 15:51:18 -04:00
football2801 b8d120e74c chore(devices): TEMP power_telemetry at error level to flush fingers_crossed
CI / test (push) Has been cancelled
Prod monolog wraps the main handler in fingers_crossed(action_level: error),
so info-level logs sit in a 50-message buffer and get discarded at request
end. That's why three deploy+bounce cycles produced zero power_telemetry
lines in docker logs despite the unconditional log call. Logging at error
level forces the buffer to flush to php://stderr → docker logs. Will be
demoted when the surrounding TEMP block is removed.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-15 15:50:43 -04:00
football2801 828332f740 design: lighten v2 overlay so the harbor is actually visible
CI / test (push) Has been cancelled
Overlay was 0.72 multiply with very dark color-bg, which crushed the
photo to invisible. Dropped to 0.48 + bumped photo brightness 0.42→0.60.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 15:50:12 -04:00
football2801 d37f5a9538 design: bring back the Camogli harbor backdrop in v2
CI / test (push) Has been cancelled
Body uses the harbor as a fixed full-page photo with brightness 0.42 +
the theme's color overlay at 0.72 opacity (multiply blend) for the dusk
tint. Applies via the shared wevisto-design.css so both Twig pages and
the SPA get it in one go.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 15:49:09 -04:00
football2801 7dab3fd7c5 remove(design): drop 'a frame, gifted · v 0.4' tagline from side rail
CI / test (push) Has been cancelled
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 15:48:16 -04:00
football2801 d7889be25a fix(design-v2): side rail footer pushed to bottom
CI / test (push) Has been cancelled
Tabs container had v1's flex:1, eating the vertical space and pinning
the footer right under the nav items. Resetting __tabs flex to 0 so
margin-top: auto on __foot can push it to the rail's bottom edge.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 15:41:52 -04:00
football2801 29cd5a4775 fix(design-v2): !important on side rail subtitle + footer display
CI / test (push) Has been cancelled
Vue scoped CSS hides the subtitle and footer by default; my design-v2
override of display:block tied on specificity and lost to source order.
!important on the v2-only display rules forces the show.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 15:40:13 -04:00
football2801 0bc6f389cc fix(design-v2): side rail subtitle + footer signature + theme swatch harbor
CI / test (push) Has been cancelled
Mockup vs live diff revealed:
- Side rail brand area: mockup has italic '— home/library/settings' subtitle
  below WeVisto, live had none. Added via reactive activeSub computed.
- Side rail footer: mockup has 'a frame, gifted · v 0.4' italic at the
  bottom of the rail. Added top-nav__foot element; hidden in horizontal
  layouts, shown in v2 side rail via design-v2.scss
- Side rail mark size: bumped from 36px to 44px to match mockup
- Theme swatch harbor preview wasn't winning the cascade fight against
  Vue scoped styles (equal-specificity tie, Vue cascades later). Adding
  !important on the few preview properties — v2 is opt-in so this is
  appropriate.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 15:38:42 -04:00
football2801 7081d2bee7 chore(devices): TEMP unconditional power_telemetry log + all X- headers
CI / test (push) Has been cancelled
Cycle 4+ polls landed cleanly but power_telemetry stayed silent even
after a php opcache bounce, so the question is whether the firmware
is sending X-Prev-* at all or the controller is filtering them out.
Log unconditionally on every poll and include every X-* request
header so the next poll shows definitively what's arriving over
the wire. Tighten back once diagnosed.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-15 15:32:04 -04:00
football2801 0489028486 refactor(design): single source of truth — wevisto-design.css
CI / test (push) Has been cancelled
v2 tokens were duplicated: in design-v2.scss for the SPA, inlined in
login.html.twig for Twig. Two places to keep in sync.

Now: one shared /public/css/wevisto-design.css loaded by every Twig
standalone template AND linked from the SPA index.html. It contains:
- Brand constants (yellow / navy / fonts)
- v2 tokens with per-theme dusk overrides
- v2 base body bg + editorial typography defaults
- v2 overrides for the .card / .btn / .field-error / .logo-badge
  patterns used across all Twig templates

The SPA's design-v2.scss now holds only SPA-specific composition:
side rail at desktop, frame card, theme swatch harbor preview,
settings polish. No token duplication.

Result: changing a v2 color in one file flows to every surface in both
worlds. Adding v2 to another Twig template only requires the existing
shared CSS link (already wired up to all 11 standalone templates).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 15:24:45 -04:00
football2801 0e4d78471b fix(design-v2): apply v2 styles to login template
CI / test (push) Has been cancelled
Twig login page was hardcoded with v1 cream/terracotta colors and had
no [data-design=v2] overrides. Cookie + html attribute injection were
working but had nothing to style off. Adding v2 overrides inline so
the login page matches the v2 register the rest of the app uses.

Default v2 palette on login uses ocean-dusk values (navy + cream text +
yellow V brand) since user isn't logged in yet so we can't read their
theme preference.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 15:19:41 -04:00
football2801 645291c724 fix(design-v2): match approved mockups — solid navy, side rail, editorial type
CI / test (push) Has been cancelled
v2 was 'harbor backdrop everywhere + glass cards', but the approved
mockups at _design/atmospheric-redesign/ use solid navy with subtle
gradient, harbor selectively (theme swatches, frame heroes), left
side rail at desktop, and editorial Marcellus/Cormorant typography.

This rewrite:
- Drops the full-page harbor backdrop; body is now solid navy with
  a single radial gradient highlight
- Loads Marcellus, Cormorant Garamond, DM Mono via Google Fonts
- Editorial type recipes: h1/h2/h3 + frame card name + settings title
  use Marcellus. settings__hint becomes italic Cormorant. Section
  labels become DM Mono caps with 0.28em letterspacing
- TopNav restyled at desktop (≥960px) into a left-fixed side rail:
  240px wide, vertical stack of nav items, active item shows inset
  yellow rule + surface bg. Body gets 240px padding-left to shift
  content right.
- Theme swatches reuse the harbor.jpg inside their preview area,
  tinted to each dusk's color — matches the mockup exactly
- Per-dusk surface colors made opaque (was rgba 0.55) so cards are
  fully readable

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 14:55:19 -04:00
football2801 4f78ed8897 fix(pwa): manifest background_color → navy to kill the white frame
CI / test (push) Has been cancelled
iOS / Chrome use background_color to fill any padding when displaying
the install icon. Cream (#fdf6ee) was leaving a visible 'thick white
border' around the V-viewfinder when the browser auto-padded the icon
on the install-preview screen. Setting background to navy (#0e2740) —
matching the icon's outer fill — makes any auto-padding invisible.

Theme_color also updated for consistency on the install splash.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 14:46:08 -04:00
football2801 77c51586e8 feat(brand): V-viewfinder 3a — the approved rebrand (was: split-W)
CI / test (push) Has been cancelled
The V-viewfinder was approved during the favicon-v2 picker at
/v2/, then deployed prematurely (e7b9756) and reverted (81effca) after
the 'design pick is not a deploy command' lesson. Deploying it now with
explicit go-ahead.

Files: yellow V outline with the Camogli harbor visible inside, navy
field outside. Replaces the split-W (two Vs forming a W) across:
- favicon-16/32/64
- apple-touch-icon (180)
- icon-192 + icon-512 manifest icons
- icon-512-maskable (V at 65% safe zone)
- favicon.svg vector
- favicon.ico multi-res
- root-level apple-touch-icon{,-precomposed}.png for iOS fallback paths

Cache-bust query bumped to ?v=20260515-vviewfinder so browsers refetch.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 14:42:00 -04:00
football2801 5e8d9efb7b fix(pwa): cache-bust icon link tags (?v=20260515-3a)
CI / test (push) Has been cancelled
iOS Safari caches the apple-touch-icon per origin and ignores byte-level
changes on the same URL. Adding a version query forces a refetch on
fresh visits without renaming the source files. Buttressed across all
standalone Twig templates and the SPA index plus the manifest icons so
Chrome desktop also refetches.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 14:37:56 -04:00
football2801 8ae4a2fd5a chore(devices): TEMP log power-monitor telemetry headers from firmware
CI / test (push) Has been cancelled
Firmware emits X-Prev-Awake-Ms + X-Prev-Panel-Init-Ms on each poll
so we can verify the 13.3" PIN_PWR rail-cut change isn't slowing
the panel re-init or extending the awake window. Log them with
boot-reason context via `device.poll.power_telemetry` — greppable
in `docker logs pictureframe-php-1`. Remove this log + the
firmware NVS_KEY_PREV_* writes once the change is validated.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-15 14:15:50 -04:00
football2801 76c72f69d8 fix(design-v2): theme swatches preview their own dusk
CI / test (push) Has been cancelled
Active-theme-only override made all six swatches show the same amber tint
in v2. Per-swatch rules via aria-label now give each its own dusk preview.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 14:05:33 -04:00
football2801 b53c0593c0 fix(design-v2): visibility polish — stronger glass + themed nav chrome
CI / test (push) Has been cancelled
First-cut v2 had transparent cards (alpha 0.55) that disappeared into the
harbor backdrop, plus the v1 cream chrome leaked through.

This pass:
- Bumps --color-surface / surface-2 to 0.85 alpha across every dusk so
  cards stay readable on top of the photo
- Darkens backdrop with a vignette + bumps --color-bg overlay to 0.70
- Adds [data-design=v2] chrome rules: top-nav/bottom-nav glass, theme
  swatches use dusks tokens, design-toggle cards become glass, install
  button keeps accent fg color, frame-card gets inset highlight
- Brightens --color-text-muted in each dusk for legibility

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 14:02:37 -04:00
football2801 a302ac09b4 feat(design): v2 opt-in (atmospheric dusks) — Settings toggle, cookie-mirrored
CI / test (push) Has been cancelled
Lets users opt into the new atmospheric design without affecting users on v1.
Adds a beta-flag toggle in Settings → Design. Server-side preference persists
across devices; a cookie mirrors it so unauthenticated Twig pages do correct
first-paint without an extra DB roundtrip.

Backend:
- User.designVersion column (nullable VARCHAR(10); null defaults to 'v1')
- Migration Version20260515120000
- PATCH /api/user/design endpoint accepting 'v1'|'v2', sets wevisto_design cookie
- SpaController injects data-design on <html> + refreshes the cookie on every
  SPA load (keeps cross-device pref in sync)
- Twig templates (base, login, register, help, setup, token-*) read the
  cookie via {{ app.request.cookies.get('wevisto_design')|default('v1') }}
  so login/setup pages also respect the user's design choice

Frontend:
- design-v2.scss — opt-in overlay scoped under [data-design="v2"]. Overrides
  --color-* tokens to dusk variants per theme (warm-craft → amber, ocean-dusk
  stays, etc.), adds harbor photo backdrop via body::before with theme tint
  via body::after. Glass-card blur on existing surfaces. v1 untouched.
- harbor.jpg shipped as a public asset (270KB, single-fetch, cached)
- User type gains designVersion ('v1' | 'v2')
- SettingsView toggle (Original / Atmospheric) calls the API, updates the
  data-design attribute optimistically, reverts on failure

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 12:28:44 -04:00
football2801 5bb8289a54 feat(ui): v1 desktop responsive — top app bar + content max-widths
CI / test (push) Has been cancelled
The existing PWA layout was mobile-first only: BottomNav hides at ≥960px
with no replacement, leaving desktop users with zero navigation and views
that stretch to viewport width. Fixes both:

- New TopNav.vue mirrors BottomNav (Home / Library / Settings) but renders
  as a top horizontal app bar at ≥960px only. Includes the wordmark + mark.
- App.vue includes <TopNav v-if="!route.meta.hideNav" /> alongside BottomNav
  so upload-flow hideNav: true still hides both.
- HomeView, LibraryView, SettingsView get desktop max-widths (820 / 1100 /
  720 respectively) so content centers instead of stretching to 1440+.

Same cream/terracotta theme tokens, no aesthetic change — just gives v1
proper desktop chrome. Prep for v2 opt-in landing next.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 12:17:49 -04:00
football2801 81effca22b Revert "feat(brand): V-viewfinder favicon set (replaces split-W on photo)"
CI / test (push) Has been cancelled
This reverts commit e7b97561c1.
2026-05-15 10:35:21 -04:00
football2801 e7b97561c1 feat(brand): V-viewfinder favicon set (replaces split-W on photo)
CI / test (push) Has been cancelled
Picked 3a from the favicons-and-logo-v2 iteration: a yellow V cut out of
navy with the harbor photo visible inside — "you are looking at a
photograph framed by the V". The brand's own glyph rather than initials.

What changed:
- favicon-16 / -32 / -64 / icon-192 / icon-512 / apple-touch-icon (180):
  V at 86% of canvas, navy outside, full center-cropped harbor inside,
  yellow stroke-outlined border proportional to size.
- icon-512-maskable: V at 65% of canvas (inside the Android safe zone),
  navy in the outer 35% so circle/squircle launcher masks crop navy
  pixels, not the V.
- favicon.svg: lightweight vector — yellow V outline on navy, no embedded
  photo (kept under 300 bytes so it's fast even before the build cache).
- favicon.ico: multi-resolution 16/32/64 for legacy clients.

Root-level fallbacks (public/apple-touch-icon.png + -precomposed +
favicon.{svg,ico}) updated in lock-step so iOS's Add-to-Home-Screen
probes pick up the new icon without falling back to a cached old one.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 10:28:09 -04:00
football2801 3bd0b9a5a1 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>
2026-05-15 10:25:09 -04:00
football2801 01b6007b1c design: atmospheric redesign mockups (login + SPA, six dusks)
CI / test (push) Has been cancelled
Self-contained HTML/CSS mockups proposing the brand-as-surface direction:
- Camogli harbor photo as the permanent backdrop on every authenticated view
- Six user themes rebuilt as atmospheric "dusks" (tinted overlays + accents)
  instead of cream variants
- Frosted-glass cards on top of the photo
- Same type system (Marcellus / Cormorant Garamond italic / Nunito body)
  shared between login and in-app

Includes:
- login-cinematic.html: canonical pre-auth example with Ken-Burns + grain
- spa/home.html, library.html, settings.html: in-app views with the new chrome
- spa/_tokens.css + _chrome.css: dusk system and frosted-glass primitives
- favicons/A-D: in-progress icon directions
- README.md with serving instructions and the porting checklist

Saved as a durable design artifact so the iteration survives /tmp wipes
and can be referred back to when porting to Vue.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 09:44:26 -04:00
football2801 6c9959c00d fix(pwa): serve apple-touch-icon + favicons from root, add sizes hint
CI / test (push) Has been cancelled
iOS Safari's Add-to-Home-Screen flow probes /apple-touch-icon.png at the
site root in addition to the <link rel> on the page. Those root paths
currently 302 through Symfony's auth firewall to /login, so iOS gets HTML
where it expects a PNG and falls back to whatever it cached from earlier
installs (the 1 KB placeholder icon). Dropping the real PNG (and the
-precomposed alias) directly in public/ makes nginx serve them as static
files, ahead of the firewall.

Also adds favicon.svg and a multi-size favicon.ico at the root for
browsers/bots that probe / paths instead of reading <link>, and adds
sizes="180x180" to every apple-touch-icon link so iOS doesn't have to
guess.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 00:48:01 -04:00
football2801 40581cb98b feat(brand): wire new favicon set into every standalone Twig template
CI / test (push) Has been cancelled
Login, register, help, token-approve/decline, and setup pages each have
their own <head> (don't extend base.html.twig), so updating base alone
left them without favicon refs. Adds the same four <link> tags after the
viewport meta in each standalone template.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 00:43:29 -04:00
football2801 a7e7b96465 feat(brand): split-W PWA icons + favicons (replaces purple-star placeholder)
CI / test (push) Has been cancelled
- New mark: solid "W" glyph color-split left=white / right=yellow over the
  Camogli harbor photo from logo.svg; right half reads as a "V" so the W
  alone communicates "WeVisto" at icon scale where the wordmark is illegible.
- PWA icons (192, 512, apple-touch 180) rendered full-bleed; maskable
  variant shrinks the W to the inner 65% so circle/squircle launcher masks
  crop sky and harbor pixels, not the glyph.
- Adds favicon-16/32/64 PNGs and replaces the old purple-star favicon.svg
  with a lightweight vector split-W on solid navy.
- Wires the new favicons into both the SPA (frontend/index.html) and the
  Symfony Twig base (templates/base.html.twig), replacing the Symfony
  default "sf" emoji data-URL placeholder on the login page.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 00:42:15 -04:00
football2801 e7e9202a11 feat(brand): official WeVisto logo + linked badge on user-facing pages
CI / test (push) Has been cancelled
- frontend/public/logo.svg: Camogli photo with We[V]isto knockout wordmark
  (yellow V accent), embedded base64 so the SVG is self-contained
- brand/: raw source (15.7MB Camogli original) + 900x900 crop used in the
  SVG, plus a short README documenting both
- Login, register, setup index/configure, help: linked logo badge above
  the page heading
- Email template: logo bumped to 64x64 (was 30 tall — wordmark unreadable)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 22:33:28 -04:00
football2801 2637eb19cd feat(brand): help page references WeVisto-XXXX AP SSID
CI / test (push) Has been cancelled
Coordinated with firmware rename of the provisioning AP from
PictureFrame-XXXX to WeVisto-XXXX. Recipients seeing the help page
mid-setup now see the same SSID name their phone WiFi list shows.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 22:03:15 -04:00
football2801 9c29788210 feat(brand): logo placeholder + smoke.sh defaults to wevisto.com
CI / test (push) Has been cancelled
Adds frontend/public/logo.svg as a placeholder (rendered at /build/logo.svg
after Vite build). Email template share_notification.html.twig swaps the
text "WeVisto" header for an <img> referencing /build/logo.svg via
absolute_url, so dropping in the final design swaps one file with no
template change.

bin/smoke.sh HOST now defaults to wevisto.com — legacy host still smoke-
testable via HOST=pictureframe.edholm.me bin/smoke.sh under the dual-
domain coexistence (Option C).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 21:51:43 -04:00
football2801 db80ea5262 feat(brand): swap recipient-facing pictureFrame strings to WeVisto
CI / test (push) Has been cancelled
Updated: SPA <title>, PWA manifest name/short_name, iOS web-app title,
"Install"/"Pin to home screen" copy, HomeView empty state, all Twig page
titles (login/register/setup/token/help), and the share-notification
email header. Left alone: the firmware-broadcast SSID PictureFrame-XXXX
(coordinated firmware change needed) and internal code/comment refs.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 21:42:50 -04:00
football2801 62cd3f57df chore(env): set MAILER_SENDER to noreply@wevisto.com
CI / test (push) Has been cancelled
Aligns the default sender with the public brand. Production mailer is
currently null:// so no live email is sent, but the From: header is now
consistent with the rest of the rebrand.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 21:27:57 -04:00
football2801 dd89b3d934 feat(brand): switch user-facing copy + Mercure topic prefix to wevisto.com
CI / test (push) Has been cancelled
Mercure topic identifiers updated in lockstep across PHP publisher + TS
subscriber (and their tests). Help-page setup instructions now point to
wevisto.com. Traefik already serves both hosts; this aligns the in-app
references with the public brand.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 21:27:07 -04:00
football2801 38ea9b3d06 fix(device-image): honor X-Draw-Pending to skip rotation during recovery
CI / test (push) Has been cancelled
When the firmware sends X-Draw-Pending: 1, its drawNeeded NVS flag
survived a power-loss-during-draw — it has the bytes for the previous
image in its cached /img.bin and just needs another chance to finish
painting them. Return the device's current image (no rotation advance),
which lands as a 304 since the device claims the same image-id.

Crucially this overrides the X-Boot-Reason: cold force-resync. The
typical mid-draw-interruption cause IS a reset that turns the next
wake into a cold boot, so without this override force-resync chases
a fresh image every interruption and the device cycles through the
rotation leaving torn frames on the 13.3 panel.

Locked image still wins (user intent overrides recovery). Old firmware
that doesn't send the header is unaffected — branch is gated on the
header being present.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 17:43:13 -04:00
football2801 f6321412aa test(frontend): cover ManageImageSheet debounce, StickerTray, PWA-install paths
CI / test (push) Has been cancelled
Add a test for ManageImageSheet's 200ms pendingApproval lock-release
(prevents the toggle becoming permanently disabled on a single tap),
expand SettingsView coverage to exercise the beforeinstallprompt event
path through usePwaInstall (accepted + dismissed outcomes), and add a
first pass of StickerTray tests.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 16:25:05 -04:00
football2801 409f51cc3e copy(crop): mention the recrop tool in the orientation-mismatch chip
CI / test (push) Has been cancelled
Old copy: "Switch the frame in Settings to display this crop."
That only surfaces one of the two ways out, and the less common one.

New copy: "Use the tool above to recrop for the current frame
orientation, or switch the frame in Settings to display this crop."
Recrop-here is the cheaper and usually-correct fix; settings-flip is
the fallback when the user really does want the other orientation.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 15:41:08 -04:00
football2801 45e80cf4c0 fix(manage-sheet): clearer copy + visual hierarchy in row controls
CI / test (push) Has been cancelled
Matt called out the row was confusing: lock pill said "Rotate" (sounds
like a verb), and the toggle's purpose wasn't obvious.

  - Drop the "Rotate" word entirely. Lock pill is icon-only when
    unlocked, shows "Locked" + closed padlock when locked.
  - Hide the lock pill entirely when the photo isn't approved on the
    frame (instead of rendering a disabled one) — keeps the row clean
    and reinforces that locking requires approval first.
  - Add a tiny "Show" / "Hidden" label above the toggle so the meaning
    reads before the user taps. Toggle is now the visual primary on
    the row.
  - Re-label aria-text from "Add/Remove" to "Show/Hide" to match the
    visible copy.

Test "disables the lock pill when not approved" → "hides the lock pill
when not approved". 358/358 still passing.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 15:31:07 -04:00
football2801 84642ed13f feat(library): photo + status badge + ManageImageSheet (Concept A)
CI / test (push) Has been cancelled
Library was rendering one approval chip per device per photo PLUS one
lock chip per approved device. That's O(photos × devices) buttons —
fine at one or two frames, breaks at four+ (see
_bmad-output/.../library-many-frames-design-ideas.md).

Concept A from the design memo:
  - Each photo card stays a square thumb + a single "Manage" row.
  - Manage row summarises state: "3/5 frames · 🔒 Mom's Place".
  - A corner-lock badge sits on the thumb itself when any frame has the
    image locked, so the lock status is glanceable from the grid.
  - Tapping Manage opens the new ManageImageSheet bottom sheet, which
    lists every frame with an approve toggle + per-frame lock pill.
    Lock pill is disabled until the frame is approved.

Per-photo widgets drop from O(photos × devices) to O(photos). Works
identically at 1 or 50 frames. Curation principle stays "manage photos
TO the frame" — same store calls (imagesStore.setApproval,
devicesStore.lockImage/unlockImage), just routed through the sheet
instead of inline chip rows.

10 new ManageImageSheet unit tests + LibraryView tests rewritten to
cover the sheet-open + event-forwarding flow. 358/358 frontend tests.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 15:26:41 -04:00
football2801 9854688a49 docs(mockups): static HTML wireframes for Library scaling concepts
CI / test (push) Has been cancelled
Renders the five design concepts from
_bmad-output/planning-artifacts/library-many-frames-design-ideas.md as
standalone HTML so Matt can compare them in a browser without spinning
up the SPA. Mockups live at /mockups/library/ and reuse the project's
design tokens for visual consistency:

  index.html   landing page with concept links
  current.html the chip-explosion state we're shipping today
  concept-a.html  photo + status badge → DevicePicker sheet (recommended)
  concept-b.html  device-first tab
  concept-c.html  multi-select bulk action bar
  concept-d.html  device chip filter + photo dots

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 15:08:18 -04:00
football2801 a511b89564 fix(library): show full photo in each grid thumb (square + contain)
CI / test (push) Has been cancelled
Library thumbs were locked to 4:3 with object-fit: cover, so portrait
photos got their top and bottom crop-fitted off. Switching to 1:1
cells with object-fit: contain — full photo always visible, grid still
uniform, portrait and landscape both get symmetric letterbox bars.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 14:40:49 -04:00
football2801 ad0d6c572c fix(home): preview locks aspect to panel dims + object-fit so it never overflows
CI / test (push) Has been cancelled
The large FrameCard preview let the <img> drive height (`flex: 0 0
auto`, `width: auto`, `max-*: 100%`). On wide-container layouts and on
the new V2 1200×1600 dimensions the image's intrinsic size leaked
past the card, and the max-width/max-height combo can drop aspect
ratio in some browsers.

Now: the preview container locks its `aspect-ratio` to
`panelDims(model, orientation)` — same source of truth that drives the
empty-placeholder shape — and the <img> fills the container with
`object-fit: contain`. Container shape is stable whether or not the
thumbnail has loaded; image always scales to fit, portrait or
landscape device, narrow or wide phone column.

emptyAspectStyle no longer needs to carry aspect (parent already has
it); empty-preview placeholder fills 100% of the parent now.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 14:30:16 -04:00
football2801 bea25098a0 revert(render): drop A/B tuning experiments; back to baseline pipeline
CI / test (push) Has been cancelled
Tried six experiments tuning saturation, gamma, blur, blue-channel
multiply, BLUE palette target, and WHITE palette target — each had
side effects worse than the original sky→face blue bleed we were
trying to fix (greens lost vibrance, sky went green, shadows got
more blue, etc). The baseline pipeline is the local maximum for now;
the 6-colour Spectra-6 palette is the real ceiling.

Drops RenderCompareCommand and restores RenderImageMessageHandler
verbatim from git tag render-baseline-2026-05-14.

Future render-quality work should attack the problem differently —
Lab-space color matching, semantic preprocessing, or a measured
panel-ink palette — not single-knob RGB tweaks.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 14:14:29 -04:00
football2801 89ba3515ac experiment(render): revert BLUE shift; warm WHITE palette to (255,248,230)
CI / test (push) Has been cancelled
#5 (BLUE → [8,32,220]) made sky map to WHITE which dithered greenish.
Reverting BLUE to baseline (24,64,192).

#6: warm WHITE from (245,245,240) → (255,248,230). Slight cream shift
pulls warm-toned skin pixels toward WHITE in the nearest-color match
before error diffusion has a chance to drift them toward BLUE. Sky
mapping unchanged (still maps to BLUE). Cost: real off-white pixels
might quantize toward a slightly warmer cream — should be subtle.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 14:07:24 -04:00
football2801 324f1b2641 experiment(render): revert blue_mul; shift BLUE palette target to (8,32,220)
CI / test (push) Has been cancelled
Experiment #4 (blue×0.95) made shadow regions appear MORE blue, not
less — reducing source blue still leaves positive blue error after a
BLACK mapping, and the dither spends that error on neighbours,
creating blue dither dots in dark regions.

Reverting blue_mul to 1.0. Experiment #5 takes a different attack on
the same problem: shift the BLUE palette mapping target from the
muted (24, 64, 192) to a more saturated (8, 32, 220). Doesn't change
what the panel displays (the blue ink is fixed); it just makes
Euclidean distance from skin tones to "BLUE" larger in the algorithm's
view, so the dither prefers RED/WHITE/YELLOW for borderline pixels.

Render-compare's BASELINE struct now carries its own frozen palette,
so half-A keeps the original (24,64,192) BLUE target while half-B
pulls the shifted palette from the live pipeline.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 14:03:25 -04:00
football2801 488fc3d0f4 experiment(render): revert sat/gamma/blur to baseline; blue-channel ×0.95
CI / test (push) Has been cancelled
Experiment #3 (sat 115, gamma 1.2, blur 0.6) was a net loss — greens
desaturated, no help on sky→face blue bleed.

Reverting those three to baseline (130, 1.0, 0.0). New experiment #4:
multiply the source's blue channel by 0.95 before dither. Real sky
stays well above the BLUE-vs-WHITE boundary, but borderline-bluish
skin (sky cast on outdoor faces) drops below it and maps to YELLOW /
WHITE / RED instead of feeding the dither's BLUE attractor.

Adds public BLUE_CHANNEL_MUL constant; render-compare's baseline
struct gets blue_mul=1.0 so half-A is still frozen at the original.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 13:54:11 -04:00
football2801 a37edcb6c7 experiment(render): revert FS + add pre-dither blur 0.6; A/B vs frozen baseline
CI / test (push) Has been cancelled
Riemersma dither produced visible "ink-spill" Hilbert-curve streaks in
low-contrast regions like skin. Reverting DITHER_METHOD to Floyd-
Steinberg and attacking the original sky-bleeds-into-face problem with
a pre-dither Gaussian blur instead:

  DITHER_METHOD  RIEMERSMA → FLOYDSTEINBERG  (back to baseline)
  + new BLUR_SIGMA = 0.6     (pre-dither softening; baseline 0.0)

Live tunables are now public so RenderCompareCommand can mirror them.
Half-A of app:render-compare is locked to the frozen-baseline set
(sat=130 gamma=1.0 sharpen=0.8 blur=0.0 FS) — what shipped at git tag
render-baseline-2026-05-14. Half-B always tracks the live pipeline, so
each future experiment is automatically the next A/B without touching
the comparison command.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 13:42:54 -04:00
football2801 2f3527aaf9 tool(render): app:render-compare for FS vs Riemersma A/B on the panel
CI / test (push) Has been cancelled
Stacks two dither treatments of the same image's top half into one V2
portrait .bin — top half Floyd-Steinberg, bottom half Riemersma —
overwrites the device's current V2 portrait asset, bumps rendered_at
and clears the preview PNG cache.

Usage: bin/console app:render-compare <imageId>

Lets Matt eyeball both methods on a single panel refresh instead of
two re-renders and two waits. One-shot experimental tool; not part of
the live render pipeline.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 13:36:12 -04:00
football2801 f3bf49ba1d experiment(render): Riemersma dither to stop sky→face blue bleed
CI / test (push) Has been cancelled
Floyd-Steinberg's row-order error diffusion was flushing residual blue
debt from sky pixels downward into the face below — visible as a blue
hue in skin tones that should have been YELLOW/RED/WHITE in the 6-color
palette. Riemersma uses a Hilbert-curve scan, so error stays local and
isn't biased along any axis.

DITHER_METHOD  FLOYDSTEINBERG → RIEMERSMA  (gamma 1.2 + saturation 115
from experiment #1 unchanged).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 13:29:46 -04:00
football2801 1ebc9b615d experiment(render): extract tunables + gamma 1.2, saturation 115%
CI / test (push) Has been cancelled
Make the render-pipeline knobs Matt and I are about to iterate on
visible as class constants on RenderImageMessageHandler — single source
of truth, easy diff per change.

First experiment (vs baseline git tag render-baseline-2026-05-14):
  SATURATION_PCT  130 → 115   (less risk of ruddy faces / synthetic skies)
  GAMMA           1.0 → 1.2   (gentle midtone lift; faces + shadows
                                climb out of the BLACK cluster after dither)

Sharpening (0.8) and Floyd-Steinberg dithering unchanged this round.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 13:22:06 -04:00
football2801 82a42011d8 fix(upload): persistent file <input> to survive iOS PWA cold launch
CI / test (push) Has been cancelled
A dynamically-created <input type="file"> that's never attached to the
DOM drops its first `change` event on a cold-launched iOS PWA — the
native photo picker resolves out of the original user-gesture context
and the closure that captured the input is gone. Symptom Matt hit
2026-05-14: first image-pick after hard-close + reopen of the PWA
silently failed to advance to the crop tool; the second attempt worked.

HomeView and LibraryView now keep a hidden <input ref="fileInputEl"
type="file"> live in their templates. onAddPhoto clicks that input
inside the user-gesture context; @change fires reliably even on cold
launches. The picker resets input.value between selections so picking
the same file twice still fires.

Tests updated to query the template input via wrapper.find() instead
of stubbing document.createElement; 347/347 frontend tests pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 13:02:26 -04:00
football2801 e57e711fcc chore(build): rebuild bundle for v2 crop fix + tighten Device.model
CI / test (push) Has been cancelled
The crop-aspect fix didn't reach production on the prior deploy because
public/build/ was 5 days stale. Rebuilds the SPA bundle so the
panelDims-driven CropEditor / StickerCanvas / FrameCard ship.

Also makes Device.model required in the TS type (was optional in this
session's first cut to placate test fixtures) and adds `model: 'v1'` to
every test Device fixture. A new device row from the API always has a
model, so the type should reflect that — leaving it optional was a trap
for production code that defensively assumed undefined.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 12:29:12 -04:00
football2801 081ca83613 fix(v2): preview rotation + crop aspect for 13.3" hardware
CI / test (push) Has been cancelled
Two related bugs that surfaced on the first 13.3" device's first photo:

1) Web-UI portrait preview was 90° sideways. DeviceApiController::
   renderBinToPng rotated whenever the device was Portrait — correct
   for V1 (landscape-native, Portrait => renderer rotated, so preview
   un-rotates) but wrong for V2 (portrait-native — the renderer
   doesn't rotate, so the preview shouldn't either). Now mirrors the
   render-pipeline check: rotate only when `orientation !==
   model->nativeOrientation()`. Two new functional tests pin the V2
   portrait and V2 landscape PNG dimensions to guard against
   regressions.

2) Cropped photo letterboxed on the 13.3" panel. CropEditor /
   StickerCanvas / FrameCard had V1 dimensions hardcoded (1600×960
   = 5:3 aspect). V2 is 4:3 (1200×1600 portrait / 1600×1200
   landscape), so a "full crop" came out the wrong shape and the
   server's white-canvas composite added bars. New `panelDims(model,
   orientation)` helper in @/types is the single source of truth on
   the frontend; matches DeviceModel::width/height on the server.
   Threaded `model` through Device serializer → Device type →
   UploadView → CropEditor / StickerCanvas, and HomeView → FrameCard.
   FrameCard tests updated to cover all four model × orientation
   placeholders.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 12:02:39 -04:00
football2801 b286a1f241 feat(devices): DeviceModel::V2 for Waveshare 13.3" Spectra-6
CI / test (push) Has been cancelled
Adds the second panel model alongside V1 (800x480, 7.3"). V2 is
1200x1600 panel-native (tall) — the inverse aspect ratio means
its "natural" orientation is portrait, not landscape:
- DeviceModel::nativeOrientation() — V1 returns Landscape, V2 returns
  Portrait. Render rotates the source image 90 CCW only when the user's
  orientation differs from the panel's native, so the .bin stays
  panel-native scan order without per-model branches.
- DeviceModel::panelId() / fromPanelId() — string mapping for the
  firmware's X-Panel-Id header (matches -DPANEL_ID build flag).
- DeviceImageController: on every poll, if X-Panel-Id maps to a known
  model and differs from the device's current model, auto-correct.
  New Devices are created with the V1 default, so a freshly-claimed
  13.3" unit needs this correction before the first image render
  produces a wrong-dimension .bin the firmware would reject.

8 new DeviceModel unit tests, 3 new controller tests cover the
header-correction behaviour (different, same, unknown panel-id).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 15:53:59 -04:00
football2801 2adb07518c feat(account): change-password endpoint + Settings modal
CI / test (push) Has been cancelled
PATCH /api/user/password — verifies the current password, enforces
8-char minimum on the new one, and rehashes via the configured
password hasher. Returns 204 on success, 422 with an `error` body
on every validation failure (wrong current, too-short new, missing
fields).

Settings adds a "Change password" link under the Account section
that opens a modal with current/new/confirm fields and posts to the
new endpoint. Confirm-mismatch and submit-disabled wiring is
client-side; backend errors surface inline.

Tests: 4 new controller tests cover success, wrong-current,
short-new, and missing-fields; success path also re-fetches the
user and checks the hash actually changed.
2026-05-09 15:25:54 -04:00
204 changed files with 8601 additions and 421 deletions
+1 -1
View File
@@ -45,7 +45,7 @@ MESSENGER_TRANSPORT_DSN=doctrine://default?auto_setup=0
###> symfony/mailer ###
MAILER_DSN=null://null
MAILER_SENDER=noreply@pictureframe.edholm.me
MAILER_SENDER=noreply@wevisto.com
###< symfony/mailer ###
###> pictureframe ###
@@ -0,0 +1,159 @@
# Library page — scaling to many frames
**Date:** 2026-05-14
**Facilitated by:** BMAD design team
**Active personas:** 🎨 Sally (UX), 🎨 Maya (Design Thinking), 📋 John (PM), 🏗️ Winston (Architect)
**Brief from {{user_name}}:** Library doesn't scale beyond 1-2 frames. Need design ideas.
---
## 1. The problem, stated by the user reading the screen
🎨 **Sally:** *"Picture Aunt Carol. She's gifted PictureFrame units to her two grown kids and three grandkids — five frames in the wild. She opens the Library page, ready to approve last weekend's birthday photos. Each photo card stacks five 'Living Room', 'Kitchen', 'Bedroom', 'Mom's Place', 'Cabin' chips PLUS five lock chips. With twenty photos on screen that's two hundred buttons. She squints, taps the wrong one, undoes it. The page feels broken — and Aunt Carol is the literal target user."*
The current per-photo layout (`<button v-for="device in devicesStore.devices">` rendered **twice** — approval row + lock row) has O(photos × devices) chips on the page. Two of the most important user-facing screens (Library "My Photos" tab) breaks down at N=4+ frames.
---
## 2. Reframing — what job is the user really hiring this page for?
📋 **John:** *Cuts to the chase.* "Three jobs we keep conflating in the same view:
| # | Job | Frequency | Trigger |
|---|-----|-----------|---------|
| J1 | 'Send THIS new photo to ALL my family frames' | high | just uploaded |
| J2 | 'Send THIS photo to ONE specific frame' | medium | birthday card for Mom |
| J3 | 'What's on Grandma's frame right now? Lock it.' | medium | curating one frame |
| J4 | 'Remove this old photo from my frames' | low | cleanup |
J1 is the bulk action — currently requires N taps. J3 is the per-frame view — currently requires scanning the chip column. The Library only directly serves J2 well, and only at small N."
🎨 **Maya:** *Sketching in the air.* "We're solving a **many-to-many curation** problem with a **per-cell grid of toggles**. Toggle grids are the worst answer at scale — visual noise scales with N×M. We should split the matrix: either device-first, photo-first-with-summary, or operation-first (bulk)."
---
## 3. Five design concepts
### 🅐 Photo card → photo-only + status badge (recommended baseline)
🎨 **Sally:** "Strip the card to the photo, plus a single line of status: `✓ on 3 of 5 frames · 1 lock`. Tap the photo opens a bottom sheet with the full grid of approvals + locks. The DevicePicker bottom sheet we already use during upload IS this exact UI — reuse it."
```
┌───────────────┐
│ │
│ [ photo ] │
│ │
└───────────────┘
✓ 3/5 frames · 🔒 Mom's
[edit] [delete]
```
- **Pros:** O(photos) widgets, not O(photos × devices). Reuses existing DevicePicker. Grid scales arbitrarily.
- **Cons:** One more tap for the "approve to one specific frame" job (J2). One-frame and two-frame users see a regression.
- **Effort:** Small. Mostly CSS + reusing DevicePicker. ~1 day.
### 🅑 Device-first column ("Per frame" tab)
📋 **John:** "Add a top-level toggle to the Library: `My Photos` / `By Frame`. The `By Frame` view is a horizontal swipe-able stack — one card per frame, each card shows its approved photos and the locked photo highlighted. Lets users curate per-frame without scanning a chip column."
```
┌── Living Room (5 approved · 🔒 Birthday) ──┐
│ [photo][photo][photo][photo][photo] [+] │
└────────────────────────────────────────────┘
┌── Kitchen (12 approved · 🔒 none) ─────┐
│ [photo][photo][photo][photo][photo] [+] │
└────────────────────────────────────────────┘
```
- **Pros:** Native answer to J3 ("what's on Grandma's frame?"). Each card is bounded — no matrix blow-up.
- **Cons:** Doesn't help J1 (one photo → all frames) without leaving this view. Extra navigation cost.
- **Effort:** Medium. New route/component + API filter param. ~2-3 days.
### 🅒 Multi-select + bulk action bar
🎨 **Maya:** *Jazz hands.* "Tap-and-hold a photo to enter selection mode. Top bar morphs into 'N selected — Send to… / Lock to… / Delete'. Tap-and-pick more photos. One trip to DevicePicker covers any batch. J1 in two gestures."
```
[grid of photos with selection bubbles in corners]
─────────────────────────────────────────
3 selected [Send to →] [Cancel]
```
- **Pros:** Solves J1 efficiently. Familiar pattern (iOS Photos, Google Photos).
- **Cons:** Discoverability — first-time users won't know to long-press. Probably worth a hint on first run.
- **Effort:** Medium. Selection-state in store + bulk API endpoint. ~2 days.
### 🅓 Device chip filter + photo dots
🎨 **Sally:** "Top of the Library page: a horizontal scrolling row of device chips. Tap one → grid filters to photos approved on that frame. Each photo's bottom shows tiny initial-dots for the OTHER frames it's on. Tap a photo to manage."
```
[All] [Living Room ✓] [Kitchen] [Bedroom] [Mom's] [Cabin]
─────────────────────────────────────────
[photo · ●LR ●KT] [photo · ●LR]
[photo · ●LR ●KT] [photo · ●LR ●MP]
```
- **Pros:** Helpful for J3 by filter. Compact status without buttons. Works at any N.
- **Cons:** Filter is implicit — user might not realise. Initial-dots crowd at high N.
- **Effort:** Medium. Filter is purely client-side; dots are styling. ~1.5 days.
### 🅔 Device groups ("Family", "Kids", "All")
📋 **John:** *Skeptical.* "If users actually have 8 frames, they probably cluster: 'all kids' rooms', 'parents' places'. Let them define one-tap groups. Photo approval/lock takes group as a unit."
```
Groups: [All Frames] [Kids' Rooms] [Parents] [+ Group]
```
- **Pros:** Aligns to the user's mental model when they have many frames.
- **Cons:** New entity to model (Group → Device many-to-many). New CRUD. Premature without evidence of cluster usage.
- **Effort:** Large. Schema change + UI for group management. ~4-5 days.
---
## 4. Architect's gut-check
🏗️ **Winston:** *Pragmatic.* "From the data side:
- **🅐 (photo-only card + sheet)** — zero schema changes, zero API changes. Pure frontend refactor. Ships fastest. Lowest risk to existing 7.3" beta units.
- **🅑 (device-first tab)** — needs a `GET /api/devices/{id}/photos` endpoint. Trivial to add.
- **🅒 (multi-select bulk)** — needs `PATCH /api/images/bulk-approve` with `[imageIds]` × `[deviceIds]`. Small server lift.
- **🅓ⓔ** — purely frontend.
If we ship one thing, **🅐 is the foundation everything else builds on** — once the per-photo card is compact, layering bulk-select (🅒) or filtering (🅓) on top is additive, not a rewrite.
I would NOT do 🅔 yet — premature abstraction. Wait for actual users with 8+ frames asking for it."
---
## 5. Where the team lands
🎨 **Sally + 📋 John + 🏗️ Winston, in unison:**
**Ship 🅐 first.** It's the smallest change that unblocks scale and **costs nothing** for the existing 1-2-frame users (their card just looks cleaner). The DevicePicker sheet is already built — point the new "manage" tap at it.
**Then 🅒 (multi-select)** as a fast-follow for J1 efficiency. Tap-hold to enter selection, top action bar, batch send. Doesn't need a server endpoint immediately — can client-loop the existing per-image PATCH.
**Defer 🅑 (device-first tab)** until a user explicitly asks "how do I see what's on Grandma's frame?". The compact photo card + DevicePicker handles J3 well enough at N≤5.
**Defer 🅓 (chip filter) and 🅔 (groups)** until usage data says they're worth it.
---
## 6. Open questions for {{user_name}}
🎨 **Sally:** "Before we wireframe — three quick ones:
1. **Status badge wording.** `✓ on 3 of 5 frames` or `3 frames`? Subtle but it sets tone (`✓ on` reads as confirmation; bare `3 frames` reads as info).
2. **Lock indicator placement.** Inside the badge line (`✓ 3/5 · 🔒 Mom's`) or as a corner overlay on the photo (lock icon top-right)?
3. **Tap target.** Does tapping the *photo* open manage, or do we want a dedicated `Manage ▸` chip below it? Photo-tap is friendlier on mobile; dedicated chip is more discoverable."
🎨 **Maya:** "And one for after we wireframe — what's the maximum frame count we should *actually* design for? If it's 'family of five = five frames', design priorities differ from 'church group of forty'."
---
## 7. Next step
If you greenlight 🅐: I'll draft the wireframe + interaction spec, then implement. Estimated 1 working session for the refactor + DevicePicker rewiring.
If you want to riff on the concepts above first, just say the word — we can pull in Carson for a wild-ideas round, or Victor (Innovation Strategist) for a disruptive frame.
+67
View File
@@ -0,0 +1,67 @@
# WeVisto · atmospheric redesign
Iterative design proposal landed on 2026-05-15. Captures the **brand-as-surface**
direction: the Camogli harbor photograph is the actual background of every
authenticated view, frosted-glass cards float on it, and the six user themes
get rebuilt as atmospheric **dusks** (tinted filters over the same photo)
instead of cream variants.
## What's here
```
atmospheric-redesign/
├── index.html — entry point, links to every mockup
├── login-cinematic.html — pre-auth canonical example
├── spa/
│ ├── home.html — HomeView with new chrome + frame card + up-next strip
│ ├── library.html — LibraryView empty state (mark in halo)
│ ├── settings.html — SettingsView with the dusk picker
│ ├── _tokens.css — type + dusks (replaces cream theme system)
│ └── _chrome.css — frosted-glass app bar / bottom nav / signature
├── favicons/ — A/B/C/D icon directions (still iterating)
└── assets/
├── harbor.jpg — the brand photograph (extracted from logo.svg)
├── wordmark.svg — full WeVisto wordmark + harbor
├── mark-photo.png — split-W on harbor (512px)
├── mark-photo-64.png — same, 64px for small uses
└── mark-navy.svg — split-W on solid navy (lightweight vector mark)
```
## To view
```bash
cd webApp/_design/atmospheric-redesign
python3 -m http.server 8766 --bind 0.0.0.0
# → open http://<host>:8766/
```
Every SPA mockup defaults to Ocean Dusk and has a floating dusk-switcher
in the bottom-right — click any chip to flip the room to that dusk.
## The principle
**Brand layer (fixed):** the harbor photograph, the wordmark, the yellow V,
the split-W mark. Permanent fixed backdrop. Never changes with theme.
**Theme layer (yours):** six dusks — Ocean, Amber, Sage, Rose, Mauve, Honey.
Each is an `rgba` multiplied over the photo plus a theme-coloured accent for
primary CTAs. Same room, different time of day.
## Open work
- **Favicon mark** — A/B/C/D directions saved; Matt's feedback was that the
small mark needs more WeVisto presence "in the image itself", not just
initials. Next iteration to integrate the wordmark into the mark.
- **Logo usage across app** — currently the full wordmark logo only appears
in emails. To propagate to login splash / library hero / welcome moments /
upload success states.
- **Implementation** — these are HTML/CSS mockups, not Vue components. The
real port needs:
- Migrate `frontend/src/styles/global.scss` from six cream themes to six
dusks (replace `--color-*` cream tokens with the `--bg-tint / --glass /
--accent` tokens shown in `spa/_tokens.css`).
- Apply the harbor backdrop via `body::before` in global.scss.
- Add the top app bar component (mark + wordmark + sub + actions).
- Rebuild HomeView / LibraryView / SettingsView to use `.glass` surfaces.
- Update the Twig login / register / setup templates to share the same
backdrop + frosted-glass card pattern.
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: 5.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 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: 470 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 905 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

@@ -0,0 +1,8 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
<rect width="64" height="64" rx="10" fill="#fdf6ee"/>
<rect x="4" y="4" width="56" height="56" rx="7" fill="none" stroke="#1a3a5c" stroke-width="1.5"/>
<!-- house silhouette: roof V over a small base, with bright yellow accent in negative -->
<path d="M14 44 L32 18 L50 44 Z" fill="#1a3a5c"/>
<path d="M22 44 L32 28 L42 44 Z" fill="#fdf6ee"/>
<rect x="29" y="36" width="6" height="8" fill="#f0d000"/>
</svg>

After

Width:  |  Height:  |  Size: 481 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 428 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 715 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

@@ -0,0 +1,12 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
<rect width="64" height="64" rx="10" fill="#1a3a5c"/>
<!-- inner frame -->
<rect x="10" y="10" width="44" height="44" rx="3" fill="#fdf6ee"/>
<rect x="13" y="13" width="38" height="38" fill="#cfe2ea"/>
<!-- sea -->
<rect x="13" y="35" width="38" height="16" fill="#4a7a9a"/>
<!-- sun -->
<circle cx="32" cy="32" r="9" fill="#f0d000"/>
<!-- frame matt highlights -->
<rect x="10" y="10" width="44" height="44" rx="3" fill="none" stroke="#0e2640" stroke-width="0.8" opacity="0.6"/>
</svg>

After

Width:  |  Height:  |  Size: 568 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 482 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 586 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 733 B

@@ -0,0 +1,29 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
<rect width="64" height="64" rx="10" fill="#1a3a5c"/>
<!-- sky highlight -->
<rect x="0" y="0" width="64" height="36" fill="#2a5478"/>
<!-- buildings as colored vertical bars (Camogli houses are tall narrow row-houses in ochre/coral/cream) -->
<g>
<rect x="6" y="22" width="6" height="34" fill="#c97c3a"/>
<rect x="13" y="18" width="7" height="38" fill="#dba068"/>
<rect x="21" y="14" width="6" height="42" fill="#e8c889"/>
<rect x="28" y="20" width="7" height="36" fill="#c45230"/>
<rect x="36" y="16" width="6" height="40" fill="#dba068"/>
<rect x="43" y="22" width="7" height="34" fill="#c97c3a"/>
<rect x="51" y="19" width="6" height="37" fill="#e8c889"/>
<!-- tiny dark windows for character (only readable at >=32) -->
<g fill="#1a3a5c" opacity="0.85">
<rect x="8" y="28" width="2" height="2"/><rect x="8" y="36" width="2" height="2"/><rect x="8" y="44" width="2" height="2"/>
<rect x="15" y="24" width="2" height="2"/><rect x="15" y="32" width="2" height="2"/><rect x="15" y="40" width="2" height="2"/><rect x="15" y="48" width="2" height="2"/>
<rect x="23" y="20" width="2" height="2"/><rect x="23" y="30" width="2" height="2"/><rect x="23" y="40" width="2" height="2"/><rect x="23" y="50" width="2" height="2"/>
<rect x="30" y="26" width="2" height="2"/><rect x="30" y="36" width="2" height="2"/><rect x="30" y="46" width="2" height="2"/>
<rect x="38" y="22" width="2" height="2"/><rect x="38" y="32" width="2" height="2"/><rect x="38" y="42" width="2" height="2"/><rect x="38" y="52" width="2" height="2"/>
<rect x="45" y="28" width="2" height="2"/><rect x="45" y="38" width="2" height="2"/><rect x="45" y="48" width="2" height="2"/>
<rect x="53" y="25" width="2" height="2"/><rect x="53" y="35" width="2" height="2"/><rect x="53" y="45" width="2" height="2"/>
</g>
</g>
<!-- water reflection band -->
<rect x="0" y="56" width="64" height="8" fill="#5e8cb0"/>
<!-- yellow sun above buildings (the "V" accent reduced to a single dot) -->
<circle cx="51" cy="10" r="4" fill="#f0d000"/>
</svg>

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 672 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

@@ -0,0 +1,32 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
<defs>
<radialGradient id="wax" cx="40%" cy="35%" r="65%">
<stop offset="0%" stop-color="#b04a35"/>
<stop offset="55%" stop-color="#8a2418"/>
<stop offset="100%" stop-color="#5a140a"/>
</radialGradient>
<filter id="bevel" x="-20%" y="-20%" width="140%" height="140%">
<feGaussianBlur stdDeviation="0.6"/>
</filter>
</defs>
<rect width="64" height="64" rx="10" fill="#fdf6ee"/>
<!-- wax circle with serrated edge -->
<g transform="translate(32 33)">
<circle r="24" fill="url(#wax)"/>
<g fill="url(#wax)">
<!-- subtle serration: tiny notches around perimeter -->
<circle cx="0" cy="-24" r="1.6"/>
<circle cx="17" cy="-17" r="1.6"/>
<circle cx="24" cy="0" r="1.6"/>
<circle cx="17" cy="17" r="1.6"/>
<circle cx="0" cy="24" r="1.6"/>
<circle cx="-17" cy="17" r="1.6"/>
<circle cx="-24" cy="0" r="1.6"/>
<circle cx="-17" cy="-17" r="1.6"/>
</g>
<circle r="20" fill="none" stroke="#5a140a" stroke-width="0.8" opacity="0.65"/>
<!-- engraved V (slightly recessed/darker) -->
<path d="M-10 -10 L0 12 L10 -10" fill="none" stroke="#3a0a04" stroke-width="3.5" stroke-linecap="round" stroke-linejoin="round" opacity="0.85"/>
<path d="M-10 -10 L0 12 L10 -10" fill="none" stroke="#d8a89a" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round" opacity="0.5" transform="translate(-0.5 -0.5)"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

+262
View File
@@ -0,0 +1,262 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>WeVisto · atmospheric redesign</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: 22px; color: var(--ink); }
h2.section { margin: 56px 0 6px; 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); }
.principles { display: grid; grid-template-columns: 1fr 1fr; gap: 28px; margin: 32px 0; padding: 28px 32px; background: var(--cream-deep); border: 1px dashed rgba(42,31,21,0.25); border-radius: 8px; }
.principle h3 { font-family: 'Marcellus', serif; font-weight: 400; font-size: 18px; color: var(--navy); margin-bottom: 6px; }
.principle h3 .yellow { color: var(--yellow); }
.principle p { font-size: 14.5px; line-height: 1.55; color: var(--ink); }
.principle p strong { color: var(--navy); font-weight: 700; }
.stack { display: grid; gap: 32px; margin-top: 24px; }
.row-2 { display: grid; grid-template-columns: 1.4fr 1fr; gap: 28px; }
.row-3 { display: grid; grid-template-columns: repeat(3, 1fr); gap: 24px; }
.card { background: #fff9ee; border: 1px solid var(--rule); display: flex; flex-direction: column; overflow: hidden; }
.card .preview { position: relative; background: #06121f; }
.card .preview iframe { border: 0; display: block; }
.card.phone .preview { aspect-ratio: 9/16; }
.card.phone .preview iframe { width: 250%; height: 250%; transform: scale(0.4); transform-origin: top left; }
.card.wide .preview { aspect-ratio: 16/10; }
.card.wide .preview iframe { width: 200%; height: 200%; transform: scale(0.5); transform-origin: top left; }
.card .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); transition: background 150ms; }
.card .open:hover { background: var(--yellow); color: var(--navy); }
.card .body { padding: 22px 24px 24px; }
.card .body .roman { font-family: 'Marcellus', serif; font-size: 22px; color: var(--yellow); line-height: 1; margin-bottom: 4px; }
.card .body h3 { font-family: 'Marcellus', serif; font-weight: 400; font-size: 24px; color: var(--navy); margin-bottom: 6px; }
.card .body .surface { font-family: 'DM Mono', monospace; font-size: 10px; letter-spacing: 0.26em; text-transform: uppercase; color: var(--ink-soft); margin-bottom: 12px; }
.card .body p { font-size: 15px; color: var(--ink); line-height: 1.5; }
.card .body p em { color: var(--navy); font-style: italic; }
/* dusks reference strip */
.dusks { display: grid; grid-template-columns: repeat(6, 1fr); gap: 12px; margin-top: 24px; }
.dusk { padding: 10px; border: 1px solid var(--rule); background: #fff9ee; text-align: center; }
.dusk .swatch { aspect-ratio: 3/2; border-radius: 4px; background-image: url('assets/harbor.jpg'); background-size: cover; position: relative; margin-bottom: 8px; overflow: hidden; }
.dusk .swatch::after { content:''; position: absolute; inset: 0; mix-blend-mode: multiply; }
.dusk[data-d="ocean"] .swatch::after { background: rgba(8, 22, 38, 0.55); }
.dusk[data-d="amber"] .swatch::after { background: rgba(60, 25, 8, 0.55); }
.dusk[data-d="sage"] .swatch::after { background: rgba(20, 40, 22, 0.55); }
.dusk[data-d="rose"] .swatch::after { background: rgba(56, 16, 38, 0.55); }
.dusk[data-d="mauve"] .swatch::after { background: rgba(40, 18, 56, 0.55); }
.dusk[data-d="honey"] .swatch::after { background: rgba(48, 36, 14, 0.55); }
.dusk .name { font-family: 'Marcellus', serif; font-size: 14px; color: var(--navy); }
.dusk .italic { font-family: 'Cormorant Garamond', serif; font-style: italic; font-size: 12px; color: var(--ink-soft); }
/* favicon section (in-progress note) */
.fav-row { display: grid; grid-template-columns: repeat(4, 1fr); gap: 20px; margin-top: 24px; }
.fav { background: #fff9ee; border: 1px solid var(--rule); padding: 18px; }
.fav__head { display: flex; align-items: center; justify-content: space-between; margin-bottom: 10px; }
.fav__letter { font-family: 'Marcellus', serif; font-size: 22px; color: var(--yellow); line-height: 1; }
.fav__label { font-family: 'DM Mono', monospace; font-size: 9px; letter-spacing: 0.24em; text-transform: uppercase; color: var(--ink-soft); }
.fav h4 { font-family: 'Marcellus', serif; font-weight: 400; font-size: 18px; color: var(--navy); margin-bottom: 4px; }
.fav .desc { font-size: 13px; color: var(--ink); line-height: 1.45; margin-bottom: 12px; min-height: 50px; }
.fav .desc em { color: var(--navy); font-style: italic; }
.fav .pips { display: flex; gap: 10px; align-items: flex-end; padding: 10px 0; border-top: 1px solid var(--rule); }
.fav .pip { text-align: center; }
.fav .pip img { display: block; image-rendering: pixelated; margin: 0 auto 4px; border-radius: 2px; }
.fav .pip .cap { font-family: 'DM Mono', monospace; font-size: 9px; color: var(--ink-soft); }
.wip { padding: 16px 20px; background: rgba(240,208,0,0.18); border-left: 4px solid var(--yellow); margin-top: 16px; font-style: italic; color: var(--ink); font-size: 15px; }
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; }
.row-2, .row-3, .fav-row, .principles, .dusks { grid-template-columns: 1fr; }
}
</style>
</head>
<body>
<div class="top">
<div class="l">Design proposal · 2026-05-15 · settled</div>
<div class="name">We<span class="v">V</span>isto</div>
<div class="r">Atmospheric system<br>login → app, one room</div>
</div>
<div class="subhead">— the brand is the surface, the theme is the tint, the photograph runs through.</div>
<h2 class="section"><span class="num"></span>The principle, after several mis-tries</h2>
<p class="lede">The earlier proposals kept the app on cream while only the login was atmospheric. That made login feel like a different product. <strong>The right answer was to rebuild the theme system itself.</strong></p>
<div class="principles">
<div class="principle">
<h3><span class="yellow"></span> Brand <em>is</em> the surface</h3>
<p>The Camogli harbor photograph is the permanent fixed backdrop on every authenticated view — same as the login. Cards become <strong>frosted glass</strong> floating on it. Light text on dark, Marcellus / Cormorant italic / Nunito body, the same recipe as the login. The wordmark and split-W mark live in the persistent top app bar.</p>
</div>
<div class="principle">
<h3><span class="yellow"></span> Theme <em>is</em> the dusk</h3>
<p>The six user themes get reborn as <strong>atmospheric dusks</strong>: a tinted overlay multiplied over the same harbor photo, plus a theme-coloured accent for primary CTAs. Same room, different time of day. The yellow V is the one brand colour that survives across every dusk — wordmark, hairlines, plate numbers.</p>
</div>
</div>
<h2 class="section"><span class="num">I.</span>The dusks (replaces the six cream themes)</h2>
<p class="lede">Each chip below is the harbor photo with the dusk's tint multiplied over it — the actual recipe used in the body backdrop. Same photo, six moods.</p>
<div class="dusks">
<div class="dusk" data-d="ocean"><div class="swatch"></div><div class="name">Ocean</div><div class="italic">— the harbor</div></div>
<div class="dusk" data-d="amber"><div class="swatch"></div><div class="name">Amber</div><div class="italic">— the workshop</div></div>
<div class="dusk" data-d="sage"><div class="swatch"></div><div class="name">Sage</div><div class="italic">— the garden</div></div>
<div class="dusk" data-d="rose"><div class="swatch"></div><div class="name">Rose</div><div class="italic">— the parlor</div></div>
<div class="dusk" data-d="mauve"><div class="swatch"></div><div class="name">Mauve</div><div class="italic">— the study</div></div>
<div class="dusk" data-d="honey"><div class="swatch"></div><div class="name">Honey</div><div class="italic">— the alcove</div></div>
</div>
<h2 class="section"><span class="num">II.</span>The front door — login</h2>
<p class="lede">The canonical example of the visual language. Same harbor backdrop, deep navy filter, frosted-glass card, staggered wordmark reveal. Now also: the home page lives in the same room.</p>
<div class="stack">
<div class="card wide">
<div class="preview">
<iframe src="login-cinematic.html" loading="lazy"></iframe>
<a class="open" href="login-cinematic.html" target="_blank">Open ⟶</a>
</div>
<div class="body">
<div class="roman">II.</div>
<h3>Login · cinematic arrival</h3>
<div class="surface">templates/security/login.html.twig · register.html.twig</div>
<p>Animated wordmark reveal, frosted-glass card, yellow CTA. Respects <em>prefers-reduced-motion</em>.</p>
</div>
</div>
</div>
<h2 class="section"><span class="num">III.</span>The room — in-app views</h2>
<p class="lede">All three default to Ocean Dusk. Use the floating chip cluster in the bottom-right of any mockup to switch dusks live — the photo re-tints, glass cards re-tint, accent button changes, the wordmark and yellow V stay constant.</p>
<div class="stack">
<div class="row-3">
<div class="card phone">
<div class="preview">
<iframe src="spa/home.html" loading="lazy"></iframe>
<a class="open" href="spa/home.html" target="_blank">Open ⟶</a>
</div>
<div class="body">
<div class="roman">III·a</div>
<h3>Home</h3>
<div class="surface">/ · HomeView.vue</div>
<p>Top app bar; editorial hero ("Good morning, <em>Alice.</em>"); frame card as glass with the user's frame photo inside; up-next strip of queued thumbnails.</p>
</div>
</div>
<div class="card phone">
<div class="preview">
<iframe src="spa/library.html" loading="lazy"></iframe>
<a class="open" href="spa/library.html" target="_blank">Open ⟶</a>
</div>
<div class="body">
<div class="roman">III·b</div>
<h3>Library · empty state</h3>
<div class="surface">/library · LibraryView.vue</div>
<p>A 112px mark in a yellow halo, "A library, <em>waiting.</em>" in display serif, three-step glass card explaining how it works, signature.</p>
</div>
</div>
<div class="card phone">
<div class="preview">
<iframe src="spa/settings.html" loading="lazy"></iframe>
<a class="open" href="spa/settings.html" target="_blank">Open ⟶</a>
</div>
<div class="body">
<div class="roman">III·c</div>
<h3>Settings</h3>
<div class="surface">/settings · SettingsView.vue</div>
<p>Glass list rows. <em>Dusk picker</em> grid showing each dusk as a tinted harbor preview — click to switch the page live.</p>
</div>
</div>
</div>
</div>
<h2 class="section"><span class="num">IV.</span>The small mark — favicon directions (work in progress)</h2>
<p class="lede">Four icon directions explored so far. Matt's still iterating here — wants WeVisto to be more present <em>inside the image itself</em>, not just initials beside the brand.</p>
<div class="wip">In progress — next iteration to integrate the wordmark into the mark and propagate the wordmark logo throughout more app surfaces.</div>
<div class="fav-row">
<div class="fav">
<div class="fav__head"><span class="fav__letter">A.</span><span class="fav__label">Warmth / home</span></div>
<h4>Roof V</h4>
<div class="desc">House silhouette where the rooftop is the V. Yellow door at centre.</div>
<div class="pips">
<div class="pip"><img src="favicons/A-roof-16.png" width="32" height="32"><div class="cap">16</div></div>
<div class="pip"><img src="favicons/A-roof-32.png" width="48" height="48"><div class="cap">32</div></div>
<div class="pip"><img src="favicons/A-roof-64.png" width="64" height="64"><div class="cap">64</div></div>
<div class="pip"><img src="favicons/A-roof-180.png" width="90" height="90"><div class="cap">180</div></div>
</div>
</div>
<div class="fav">
<div class="fav__head"><span class="fav__letter">B.</span><span class="fav__label">Literal / poetic</span></div>
<h4>Frame + horizon</h4>
<div class="desc">A tiny photo frame with a sun over the sea. Most legible at 16.</div>
<div class="pips">
<div class="pip"><img src="favicons/B-frame-horizon-16.png" width="32" height="32"><div class="cap">16</div></div>
<div class="pip"><img src="favicons/B-frame-horizon-32.png" width="48" height="48"><div class="cap">32</div></div>
<div class="pip"><img src="favicons/B-frame-horizon-64.png" width="64" height="64"><div class="cap">64</div></div>
<div class="pip"><img src="favicons/B-frame-horizon-180.png" width="90" height="90"><div class="cap">180</div></div>
</div>
</div>
<div class="fav">
<div class="fav__head"><span class="fav__letter">C.</span><span class="fav__label">Origin-specific</span></div>
<h4>Camogli skyline</h4>
<div class="desc">Camogli's coloured row-houses as silhouette. Risk: generic city at 16.</div>
<div class="pips">
<div class="pip"><img src="favicons/C-skyline-16.png" width="32" height="32"><div class="cap">16</div></div>
<div class="pip"><img src="favicons/C-skyline-32.png" width="48" height="48"><div class="cap">32</div></div>
<div class="pip"><img src="favicons/C-skyline-64.png" width="64" height="64"><div class="cap">64</div></div>
<div class="pip"><img src="favicons/C-skyline-180.png" width="90" height="90"><div class="cap">180</div></div>
</div>
</div>
<div class="fav">
<div class="fav__head"><span class="fav__letter">D.</span><span class="fav__label">Gift / stationery</span></div>
<h4>Wax seal V</h4>
<div class="desc">Pressed-wax seal with V. Strong at 64+, softens at 16.</div>
<div class="pips">
<div class="pip"><img src="favicons/D-seal-16.png" width="32" height="32"><div class="cap">16</div></div>
<div class="pip"><img src="favicons/D-seal-32.png" width="48" height="48"><div class="cap">32</div></div>
<div class="pip"><img src="favicons/D-seal-64.png" width="64" height="64"><div class="cap">64</div></div>
<div class="pip"><img src="favicons/D-seal-180.png" width="90" height="90"><div class="cap">180</div></div>
</div>
</div>
</div>
<footer>
Saved at <code>webApp/_design/atmospheric-redesign/</code>. Reload any mockup to replay animations.
</footer>
</body>
</html>
@@ -0,0 +1,349 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
<title>Sign in — 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=Marcellus&family=Cormorant+Garamond:ital,wght@0,300;0,400;0,500;0,600;1,400&display=swap" rel="stylesheet">
<style>
:root {
--navy: #0e2740;
--navy-deep: #07172a;
--yellow: #f0d000;
--yellow-soft: #c4a700;
--cream: #faf3e4;
--cream-dim: #d8cbb0;
}
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
html, body { width: 100%; min-height: 100%; background: var(--navy-deep); color: var(--cream); }
body {
font-family: 'Cormorant Garamond', serif;
position: relative;
min-height: 100vh;
overflow-x: hidden;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 56px 24px;
}
/* Ken-Burns harbor backdrop */
.backdrop {
position: fixed;
inset: -4%;
background: url('/assets/harbor.jpg') center/cover no-repeat;
filter: brightness(0.5) saturate(0.85);
opacity: 0;
transform: scale(1.04);
z-index: 0;
animation:
bd-fade 1.6s 0.1s cubic-bezier(.2,.7,.2,1) forwards,
bd-zoom 28s 0.1s linear forwards;
}
@keyframes bd-fade { to { opacity: 0.42; } }
@keyframes bd-zoom { from { transform: scale(1.04); } to { transform: scale(1.13) translate(-1.5%, -0.8%); } }
.vignette {
position: fixed;
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%);
z-index: 1;
}
.grain {
position: fixed;
inset: 0;
pointer-events: none;
z-index: 2;
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;
}
/* upper stamps */
.stamp {
position: fixed;
top: 30px;
left: 32px;
font-family: 'Marcellus', serif;
font-size: 11px;
letter-spacing: 0.46em;
color: var(--cream);
opacity: 0;
text-transform: uppercase;
z-index: 10;
animation: tag-in 1.4s 0.9s ease forwards;
}
.stamp .yellow { color: var(--yellow); }
.corner {
position: fixed;
top: 30px;
right: 32px;
font-family: 'Marcellus', serif;
font-size: 11px;
letter-spacing: 0.4em;
color: var(--cream);
opacity: 0;
text-transform: uppercase;
z-index: 10;
animation: tag-in 1.4s 1.0s ease forwards;
}
@keyframes tag-in { to { opacity: 0.85; transform: translateY(0); } }
/* the central composition */
.stage {
position: relative;
z-index: 5;
display: flex;
flex-direction: column;
align-items: center;
width: 100%;
max-width: 440px;
}
/* wordmark — same staggered reveal as splash, but smaller */
.wordmark {
font-family: 'Marcellus', serif;
font-weight: 400;
font-size: clamp(48px, 10vw, 76px);
line-height: 1;
color: var(--cream);
display: flex;
justify-content: center;
align-items: baseline;
gap: 0.005em;
margin-bottom: 14px;
}
.wordmark .letter {
display: inline-block;
opacity: 0;
transform: translateY(0.35em) rotate(2deg);
animation: letter-in 1.0s cubic-bezier(.2,.7,.2,1) forwards;
}
.wordmark .v {
color: var(--yellow);
text-shadow: 0 1px 0 rgba(0,0,0,0.25), 0 12px 24px rgba(240,208,0,0.18);
}
.wordmark .letter:nth-child(1) { animation-delay: 0.45s; }
.wordmark .letter:nth-child(2) { animation-delay: 0.52s; }
.wordmark .letter:nth-child(3) { animation-delay: 0.66s; }
.wordmark .letter:nth-child(4) { animation-delay: 0.78s; }
.wordmark .letter:nth-child(5) { animation-delay: 0.85s; }
.wordmark .letter:nth-child(6) { animation-delay: 0.92s; }
.wordmark .letter:nth-child(7) { animation-delay: 0.99s; }
@keyframes letter-in { to { opacity: 1; transform: translateY(0) rotate(0); } }
/* tiny rule + tagline below wordmark */
.underline {
width: 0;
height: 1px;
margin: 6px auto 14px;
background: linear-gradient(90deg, transparent, var(--yellow), transparent);
animation: rule-grow 1.4s 1.25s cubic-bezier(.2,.7,.2,1) forwards;
}
@keyframes rule-grow { to { width: 180px; } }
.tagline {
font-style: italic;
font-weight: 400;
font-size: 17px;
color: var(--cream);
opacity: 0;
transform: translateY(8px);
letter-spacing: 0.03em;
margin-bottom: 40px;
text-align: center;
animation: tag-in 1.2s 1.7s cubic-bezier(.2,.7,.2,1) forwards;
}
/* the sign-in panel — frosted glass card */
.panel {
width: 100%;
background: rgba(7, 23, 42, 0.55);
border: 1px solid rgba(255, 240, 200, 0.18);
backdrop-filter: blur(14px);
-webkit-backdrop-filter: blur(14px);
box-shadow:
0 1px 0 rgba(255,255,255,0.06) inset,
0 30px 60px -20px rgba(0,0,0,0.65);
padding: 36px 36px 32px;
opacity: 0;
transform: translateY(16px);
animation: panel-in 1.2s 2.0s cubic-bezier(.2,.7,.2,1) forwards;
position: relative;
}
@keyframes panel-in { to { opacity: 1; transform: translateY(0); } }
.panel h1 {
font-family: 'Cormorant Garamond', serif;
font-style: italic;
font-weight: 400;
font-size: 26px;
color: var(--cream);
margin-bottom: 4px;
letter-spacing: 0.01em;
}
.panel .greeting {
font-family: 'Cormorant Garamond', serif;
font-size: 14px;
font-style: italic;
color: var(--cream-dim);
margin-bottom: 26px;
}
.field { margin-bottom: 18px; }
.field label {
display: block;
font-family: 'Marcellus', serif;
font-size: 10px;
letter-spacing: 0.32em;
text-transform: uppercase;
color: var(--cream-dim);
margin-bottom: 8px;
}
.field input {
width: 100%;
background: transparent;
border: 0;
border-bottom: 1px solid rgba(255, 240, 200, 0.28);
color: var(--cream);
font-family: 'Cormorant Garamond', serif;
font-size: 20px;
font-weight: 400;
padding: 8px 0 10px;
outline: none;
transition: border-color 0.25s ease;
}
.field input:focus {
border-color: var(--yellow);
}
.field input::placeholder { color: rgba(216, 203, 176, 0.45); font-style: italic; }
.btn {
width: 100%;
margin-top: 18px;
background: var(--yellow);
color: var(--navy-deep);
border: 0;
padding: 16px 24px;
font-family: 'Marcellus', serif;
font-size: 13px;
letter-spacing: 0.34em;
text-transform: uppercase;
cursor: pointer;
transition: background 0.25s ease, transform 0.2s ease, box-shadow 0.25s ease;
box-shadow: 0 8px 18px -4px rgba(240,208,0,0.35);
}
.btn:hover {
background: var(--cream);
transform: translateY(-1px);
box-shadow: 0 12px 24px -4px rgba(255,240,200,0.4);
}
.register-link {
margin-top: 22px;
text-align: center;
font-size: 15px;
font-style: italic;
color: var(--cream-dim);
}
.register-link a {
color: var(--yellow);
text-decoration: none;
border-bottom: 1px solid rgba(240,208,0,0.4);
padding-bottom: 1px;
}
.register-link a:hover { border-bottom-color: var(--yellow); }
/* bottom stamp */
.footstamp {
position: fixed;
bottom: 28px;
left: 0;
right: 0;
text-align: center;
font-family: 'Marcellus', serif;
font-size: 11px;
letter-spacing: 0.4em;
color: var(--cream);
opacity: 0;
text-transform: uppercase;
z-index: 10;
animation: tag-in 1.4s 2.6s ease forwards;
}
.footstamp .sep { margin: 0 10px; color: var(--yellow); opacity: 0.7; }
/* loader dots top-right */
.loader {
position: fixed;
top: 32px;
right: 32px;
display: none; /* mute loader on login — keep stamps only */
}
@media (max-width: 520px) {
.stamp, .corner { font-size: 10px; top: 20px; }
.stamp { left: 20px; letter-spacing: 0.32em; }
.corner { right: 20px; }
.panel { padding: 28px 24px 24px; }
.footstamp { bottom: 16px; font-size: 10px; }
}
@media (prefers-reduced-motion: reduce) {
.backdrop, .wordmark .letter, .tagline, .underline, .panel, .stamp, .corner, .footstamp {
animation: none !important; opacity: 1 !important; transform: none !important; width: auto !important;
}
.underline { width: 180px !important; }
.backdrop { opacity: 0.42; }
}
</style>
</head>
<body>
<div class="backdrop"></div>
<div class="vignette"></div>
<div class="grain"></div>
<div class="stamp">WeVisto <span class="yellow">·</span> Plate I</div>
<div class="corner">Sign in</div>
<div class="stage">
<div class="wordmark" aria-label="WeVisto">
<span class="letter">W</span>
<span class="letter">e</span>
<span class="letter v">V</span>
<span class="letter">i</span>
<span class="letter">s</span>
<span class="letter">t</span>
<span class="letter">o</span>
</div>
<div class="underline"></div>
<div class="tagline">Photographs, gifted. Quietly. Forever.</div>
<form class="panel" method="post" action="#" novalidate>
<h1>Welcome back.</h1>
<p class="greeting">— sign in to your account</p>
<div class="field">
<label for="inputEmail">Email address</label>
<input type="email" id="inputEmail" name="_username" autocomplete="email" placeholder="you@somewhere.com" autofocus>
</div>
<div class="field">
<label for="inputPassword">Password</label>
<input type="password" id="inputPassword" name="_password" autocomplete="current-password" placeholder="••••••••">
</div>
<button type="submit" class="btn">Sign in</button>
<p class="register-link">Don't have an account? <a href="/register">Create one</a></p>
</form>
</div>
<div class="footstamp">Edizione I <span class="sep">·</span> MMXXVI <span class="sep">·</span> A frame, by hand</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>
+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: 5.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 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>
+5 -2
View File
@@ -21,8 +21,11 @@
# the backgrounded SSE subscriber in step 5 sometimes gets killed before it
# receives data. Reproduces inside this script, not in interactive shell.
HOST="pictureframe.edholm.me"
SSH_HOST="pictureframe"
# Defaults to the new wevisto.com host; override with HOST=… to smoke-test
# the legacy pictureframe.edholm.me alias kept alive under the dual-domain
# migration (Option C).
HOST="${HOST:-wevisto.com}"
SSH_HOST="${SSH_HOST:-pictureframe}"
TESTBOT_EMAIL="testbot@example.com"
TESTBOT_PASS="testpass123"
RED="\033[31m"; GREEN="\033[32m"; RESET="\033[0m"
Binary file not shown.

After

Width:  |  Height:  |  Size: 264 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 MiB

+8
View File
@@ -0,0 +1,8 @@
# Brand source assets
| File | Purpose |
|------|---------|
| `IMG_2524.jpg` | Raw full-quality original (Camogli harbour, 5712×4284). The hero photo behind the WeVisto wordmark. Use this for any marketing, social, or print work. |
| `IMG_2524-square900.jpg` | 900×900 centred crop (vbias 0.42) — the exact image embedded as base64 in `frontend/public/logo.svg`. Regenerate the logo from this if you ever tweak the wordmark. |
The composed logo SVG lives at `frontend/public/logo.svg` (Vite copies it to `public/build/logo.svg` at build time).
+7 -4
View File
@@ -3,17 +3,20 @@
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
<title>pictureFrame</title>
<title>WeVisto</title>
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<link rel="icon" type="image/svg+xml" href="/build/favicon.svg?v=20260515-vviewfinder" />
<link rel="icon" type="image/png" sizes="32x32" href="/build/icons/favicon-32.png?v=20260515-vviewfinder" />
<link rel="icon" type="image/png" sizes="16x16" href="/build/icons/favicon-16.png?v=20260515-vviewfinder" />
<link rel="manifest" href="/build/manifest.webmanifest" />
<link rel="apple-touch-icon" href="/build/icons/apple-touch-icon.png" />
<link rel="apple-touch-icon" sizes="180x180" href="/build/icons/apple-touch-icon.png?v=20260515-vviewfinder" />
<link rel="stylesheet" href="/css/wevisto-design.css">
<meta name="theme-color" content="#fdf6ee" />
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="default" />
<meta name="apple-mobile-web-app-title" content="pictureFrame" />
<meta name="apple-mobile-web-app-title" content="WeVisto" />
</head>
<body>
<div id="app"></div>
Binary file not shown.

After

Width:  |  Height:  |  Size: 264 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 9.3 KiB

After

Width:  |  Height:  |  Size: 271 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 647 B

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.

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.2 KiB

After

Width:  |  Height:  |  Size: 115 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.1 KiB

After

Width:  |  Height:  |  Size: 190 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 352 KiB

Some files were not shown because too many files have changed in this diff Show More