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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
- 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>
- 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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>