diff --git a/.impeccable/config.json b/.impeccable/config.json index 8613286..2cf9f1f 100644 --- a/.impeccable/config.json +++ b/.impeccable/config.json @@ -27,6 +27,12 @@ "value": "plus jakarta sans", "createdAt": "2026-10-11T09:31:13.372Z", "reason": "Existing Bento Hub layout intentionally declares Plus Jakarta Sans in theme-bento-hub.css; the Search redesign preserves that established layout typography." + }, + { + "rule": "overused-font", + "value": "plus jakarta sans", + "createdAt": "2026-10-12T23:30:12.554Z", + "reason": "Bento Hub explicitly declares Plus Jakarta Sans as its layout typography in frontend/theme-bento-hub.css; this existing choice is unrelated to the Glass Stage card change." } ] } diff --git a/.impeccable/critique/2026-10-12T23-30-24Z__frontend-theme-glass-css.md b/.impeccable/critique/2026-10-12T23-30-24Z__frontend-theme-glass-css.md new file mode 100644 index 0000000..9402635 --- /dev/null +++ b/.impeccable/critique/2026-10-12T23-30-24Z__frontend-theme-glass-css.md @@ -0,0 +1,52 @@ +--- +target: Glass Stage recommendation card +total_score: 28 +max_score: 40 +na_heuristics: +p0_count: 0 +p1_count: 0 +target_identity: "file:/home/josh/development/personal/ytplayer-codex/wt-reccard/frontend/theme-glass.css" +target_fingerprint: "sha256:90049d33097797770fee252881753514e2ebfa90005d6c1f1b1157eba22e8474" +target_path: /home/josh/development/personal/ytplayer-codex/wt-reccard/frontend/theme-glass.css +timestamp: 2026-10-12T23-30-24Z +slug: frontend-theme-glass-css +--- +Method: dual-agent (A: /root/rec_card_design_critique · B: /root/rec_card_detector_evidence) + +## Design Health Score +| # | Heuristic | Score | Key issue | +|---|---|---:|---| +| 1 | Visibility of System Status | 3 | Recommendation reason is visible in-card. | +| 2 | Match System / Real World | 3 | Artwork, channel and listening reason map to familiar media cards. | +| 3 | User Control and Freedom | 3 | Queue and overflow actions remain distinct. | +| 4 | Consistency and Standards | 3 | Glass treatment is scoped; other layouts retain existing presentation. | +| 5 | Error Prevention | 3 | Missing artwork/reason remain structured and contained. | +| 6 | Recognition Rather Than Recall | 3 | Reason line explains why each item is recommended. | +| 7 | Flexibility and Efficiency | 3 | Queue is one-tap; secondary actions remain available. | +| 8 | Aesthetic and Minimalist Design | 3 | Unified surface adds clarity without extra controls. | +| 9 | Error Recovery | 2 | No card-specific recovery beyond Refresh for the section. | +| 10 | Help and Documentation | 2 | Actions rely on concise labels rather than help text. | +| **Total** | | **28/40** | Good; light-theme material could read more translucent. | + +## Design Specificity Verdict +The card now reads as a Glass Stage surface through frosted token color, rounded shell, thin outline, and warm accent actions. In light mode it is nearly opaque, favoring legibility over visible translucency. Deterministic scan ran once on `frontend/index.html`; visible global findings were existing low-contrast, gradient-text, marquee, dark-glow, and token-advisory findings. Output was truncated, and none of the visible findings identified the changed card CSS. No detector overlay was injected; Playwright screenshots supplied the visual evidence. + +## Overall Impression +The shell now contains artwork, title, channel, reason and actions as one coherent card. Long text wraps safely and the 44px actions remain easy to target. + +## What's Working +- All card children fit within the rounded card at the tested widths. +- Empty artwork has a token-based color treatment; real art fills a 16:9 frame. +- Focus rings are inset so shell clipping does not hide keyboard focus. + +## Priority Issues +- [P2] Light Glass Stage surface is nearly opaque; increase translucency only if the text AA checks remain passing. +- [P3] Refresh is section-level; no separate recovery action is needed for individual cards. + +## Cognitive Load and Emotional Journey +Low-to-moderate: image → title/channel → reason → queue action follows a clear scan path. The artwork leads, the reason reassures relevance, and Queue provides an immediate next action. + +## Persona Red Flags and Minor Notes +At 390px the Queue label is compact but readable. Classic/Bento/F7 screenshots show the existing styles; no shared CSS was changed. + +Questions skipped: the task specified the target visual language, layout scope, and acceptance criteria. diff --git a/frontend/theme-glass.css b/frontend/theme-glass.css index 4a22d05..0650918 100644 --- a/frontend/theme-glass.css +++ b/frontend/theme-glass.css @@ -384,3 +384,69 @@ html[data-layout="glass-stage"] .search-btn { box-shadow: inset 0 1px 0 color-mix(in srgb, var(--text) 12%, transparent); } html[data-layout="glass-stage"] .search-btn:hover { background: color-mix(in srgb, var(--search-action-surface) 94%, var(--accent)); border-color: var(--accent); color: var(--search-action-foreground); transform: none; } + +/* Recommendation cards: one glass surface contains the complete interaction. */ +html[data-layout="glass-stage"] .rec-grid { align-items: stretch; gap: 16px 14px; } +html[data-layout="glass-stage"] .rec-item { + display: flex; flex-direction: column; min-width: 0; height: 100%; overflow: hidden; + border: 1px solid color-mix(in srgb, var(--text) 45%, transparent); border-radius: 20px; + background: color-mix(in srgb, var(--bg-1) 92%, transparent); + -webkit-backdrop-filter: blur(22px) saturate(1.25); backdrop-filter: blur(22px) saturate(1.25); + box-shadow: var(--shadow); +} +html[data-layout="glass-stage"] .rec-play { + display: flex; flex: none; flex-direction: column; align-items: stretch; gap: 0; + width: 100%; min-width: 0; overflow: hidden; padding: 0; border: 0; border-radius: 0; + background: transparent; box-shadow: none; color: var(--text); text-align: left; +} +html[data-layout="glass-stage"] .rec-play:hover { background: transparent; } +html[data-layout="glass-stage"] .rec-cover { + flex: none; width: 100%; aspect-ratio: 16 / 9; overflow: hidden; border-radius: 0; + background: var(--bg-2); +} +html[data-layout="glass-stage"] .rec-cover:has(img[src=""]) { + background: radial-gradient(ellipse at 50% 42%, color-mix(in srgb, var(--accent) 30%, var(--bg-2)), var(--bg-2) 72%); +} +html[data-layout="glass-stage"] .rec-cover img { display: block; width: 100%; height: 100%; object-fit: cover; object-position: center; } +html[data-layout="glass-stage"] .rec-cover img[src=""] { display: none; } +html[data-layout="glass-stage"] .rec-title { + display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2; overflow: hidden; + margin: 0; padding: 15px 14px 0; color: var(--text); font: 650 14px/1.4 var(--ui); + overflow-wrap: anywhere; +} +html[data-layout="glass-stage"] .rec-channel { + display: block; min-width: 0; padding: 5px 14px 0; color: var(--text-2); + font: 400 12px/1.45 var(--ui); overflow-wrap: anywhere; +} +html[data-layout="glass-stage"] .rec-reason { + align-self: flex-start; max-width: calc(100% - 28px); margin: 12px 14px 0; padding: 6px 10px; + border: 1px solid color-mix(in srgb, var(--text) 24%, transparent); border-radius: 12px; + background: color-mix(in srgb, var(--accent) 12%, var(--bg-1)); color: var(--text-2); + font: 500 11px/1.4 var(--ui); overflow-wrap: anywhere; +} +html[data-layout="glass-stage"] .rec-actions { + display: flex; flex: none; align-items: stretch; gap: 8px; width: 100%; margin-top: auto; padding: 12px 14px 14px; +} +html[data-layout="glass-stage"] .rec-actions button { + display: inline-flex; flex: none; align-items: center; justify-content: center; + min-width: 44px; min-height: 44px; padding: 0 12px; + border: 1px solid color-mix(in srgb, var(--text) 28%, transparent); border-radius: 14px; + background: var(--bg-2); color: var(--text); font: 600 13px/1.2 var(--ui); white-space: nowrap; + cursor: pointer; transition: background-color .16s ease, border-color .16s ease, transform .16s ease; +} +html[data-layout="glass-stage"] .rec-actions button:first-child { + flex: 1 1 auto; min-width: 0; border-color: color-mix(in srgb, var(--accent) 32%, var(--bg-1)); + background: color-mix(in srgb, var(--accent) 16%, var(--bg-1)); +} +html[data-layout="glass-stage"] .rec-actions button:last-child { width: 44px; padding: 0; } +html[data-layout="glass-stage"] .rec-actions button:hover { + border-color: var(--accent-bright); background: color-mix(in srgb, var(--accent) 24%, var(--bg-1)); +} +html[data-layout="glass-stage"] .rec-actions button:last-child:hover { background: var(--bg-3); } +html[data-layout="glass-stage"] .rec-play:focus-visible { outline: none; box-shadow: inset 0 0 0 2px var(--accent-bright); } +html[data-layout="glass-stage"] .rec-actions button:focus-visible { outline: none; box-shadow: inset 0 0 0 2px var(--accent-bright); } +html[data-layout="glass-stage"] .rec-refresh:focus-visible { outline: 2px solid var(--accent-bright); outline-offset: 3px; } +html[data-layout="glass-stage"] .rec-item :is(.rec-title,.rec-channel,.rec-reason) { overflow-wrap: anywhere; } +@media (prefers-reduced-motion: reduce) { + html[data-layout="glass-stage"] .rec-actions button { transition: none; } +} diff --git a/perf/README.md b/perf/README.md index c241885..bd5b6ce 100644 --- a/perf/README.md +++ b/perf/README.md @@ -489,3 +489,10 @@ remote toggles against a mocked relay. Transport previews and remote screenshots are saved in ignored `plans/transport-shots/`. Run `node perf/ui-geometry.mjs` for the 320–430px search-bar and Saved-row geometry matrix. OS lock-screen buttons remain platform-rendered through the existing MediaSession handlers. + +Recommendation cards: `node perf/rec-card.mjs` exercises long titles/reasons, +missing artwork, card containment, 16:9 cover sizing, focus visibility, and text +contrast in Glass Stage at 320/390/768/1280px and dark/light/contrast themes. It +also captures Classic, Bento Hub, and Framework7 Swipe at 390/1280px to guard +their existing card presentation. Screenshots are written to +`plans/rec-card-shots/`. diff --git a/perf/rec-card.mjs b/perf/rec-card.mjs new file mode 100644 index 0000000..42ab70d --- /dev/null +++ b/perf/rec-card.mjs @@ -0,0 +1,138 @@ +#!/usr/bin/env node +import assert from 'node:assert/strict'; +import { createServer } from 'node:http'; +import { readFile, mkdir } from 'node:fs/promises'; +import { extname, resolve } from 'node:path'; +import { chromium } from 'playwright'; + +const root = resolve('frontend'); +const shots = resolve('plans/rec-card-shots'); +await mkdir(shots, { recursive: true }); +const mime = { '.html':'text/html', '.js':'text/javascript', '.css':'text/css', '.json':'application/json', '.woff2':'font/woff2', '.png':'image/png', '.svg':'image/svg+xml', '.webmanifest':'application/manifest+json' }; +const server = createServer(async (req, res) => { + const pathname = new URL(req.url, 'http://local').pathname; + const file = resolve(root, `.${pathname === '/' ? '/index.html' : pathname}`); + if (!file.startsWith(root + '/')) { res.writeHead(403).end(); return; } + try { const body = await readFile(file); res.writeHead(200, { 'Content-Type': mime[extname(file)] || 'application/octet-stream' }).end(body); } + catch { res.writeHead(404).end(); } +}); +await new Promise(resolveListen => server.listen(0, '127.0.0.1', resolveListen)); +const origin = `http://127.0.0.1:${server.address().port}`; +const layouts = ['glass-stage', 'classic', 'bento-hub', 'f7-swipe']; +const themes = ['dark', 'light', 'contrast']; +const failures = []; +const svg = encodeURIComponent(''); +const thumb = `data:image/svg+xml,${svg}`; +const cards = [ + { id:'one', title:'You Have Said — A Long Live Worship Recording with a Carefully Wrapped Title', channel:'Cathedral of Praise Worship', reason:'One of your most played', thumbnail:thumb }, + { id:'two', title:'A Second Recommendation with a Long Title to Verify the Two-Line Clamp', channel:'The Choir and Orchestra', reason:'Because you saved several long-form worship sessions', thumbnail:thumb }, + { id:'three', title:'Missing artwork and reason', channel:'Grace Community', reason:'', thumbnail:'' }, + { id:'four', title:'Fourth card with the normal metadata length', channel:'Hesed Worship', reason:'A familiar favorite', thumbnail:thumb }, + { id:'five', title:'Fifth card', channel:'Worship Archive', reason:'A discovery', thumbnail:thumb }, + { id:'six', title:'Sixth card', channel:'Live Sessions', reason:'More from your listening', thumbnail:thumb }, +]; + +const browser = await chromium.launch({ headless: true }); +try { + const context = await browser.newContext({ viewport: { width: 390, height: 844 }, deviceScaleFactor: 1, serviceWorkers: 'block' }); + await context.route('**/*', route => route.request().url().startsWith(origin) ? route.continue() : route.abort()); + await context.route('**/api/**', route => route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ ok: true, results: [] }) })); + await context.route('**/api/recommendations*', route => route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ ok: true, results: cards }) })); + const page = await context.newPage(); + page.on('pageerror', error => failures.push(`pageerror: ${error.message}`)); + page.on('requestfailed', request => failures.push(`requestfailed: ${request.url()} ${request.failure()?.errorText}`)); + page.on('response', response => { + if (response.url().includes('/api/recommendations') && response.status() >= 400) failures.push(`recommendations response: ${response.status()} ${response.url()}`); + }); + const matrix = []; + for (const layout of layouts) for (const theme of themes) { + const widths = layout === 'glass-stage' ? [320, 390, 768, 1280] : [390, 1280]; + await context.addInitScript(({ layout, theme }) => localStorage.setItem('_ytpdata', JSON.stringify({ playlists: [], history: [], queue: [], settings: { layout, theme } })), { layout, theme }); + await page.setViewportSize({ width: 390, height: 844 }); + await page.goto(origin, { waitUntil: 'domcontentloaded' }); + await page.waitForFunction(({ layout, theme }) => document.documentElement.dataset.layout === layout && document.documentElement.dataset.theme === theme, { layout, theme }, { timeout: 10000 }); + try { await page.waitForFunction(() => document.querySelectorAll('#homeRecommendations .rec-item').length === 6, null, { timeout: 5000 }); } + catch (error) { console.error('render debug', JSON.stringify({ layout, theme, errors: failures, state: await page.evaluate(() => ({ title: document.title, layout: document.documentElement.dataset.layout, theme: document.documentElement.dataset.theme, body: document.body.innerText.slice(0, 500), recommendation: document.querySelector('#homeRecommendations')?.outerHTML.slice(0, 1000) })) })); throw error; } + await page.locator('#homeRecommendations img').first().evaluate(img => img.decode().catch(() => {})); + for (const width of widths) { + await page.setViewportSize({ width, height: 844 }); + await page.waitForTimeout(50); + const metrics = await page.locator('#homeRecommendations').evaluate(section => { + const rgba = value => { + const channels = (value.match(/[\d.]+/g) || []).map(Number); + if (value.startsWith('color(srgb')) return [channels[0] * 255, channels[1] * 255, channels[2] * 255, channels[3] ?? 1]; + return [channels[0], channels[1], channels[2], channels[3] ?? 1]; + }; + const rgb = value => rgba(value).slice(0, 3); + const luminance = value => { + const [r,g,b] = rgb(value).map(channel => channel / 255).map(channel => channel <= .04045 ? channel / 12.92 : ((channel + .055) / 1.055) ** 2.4); + return .2126 * r + .7152 * g + .0722 * b; + }; + const contrast = (a,b) => { + const [r,g,bl,alpha] = rgba(b), composite = `rgb(${r * alpha + 255 * (1-alpha)},${g * alpha + 255 * (1-alpha)},${bl * alpha + 255 * (1-alpha)})`; + const x = luminance(a), y = luminance(composite); return (Math.max(x,y) + .05) / (Math.min(x,y) + .05); + }; + const rect = el => { const r = el.getBoundingClientRect(); return { top: r.top, bottom: r.bottom, height: r.height, width: r.width }; }; + const cards = [...section.querySelectorAll('.rec-item')].map(item => { + const play = item.querySelector('.rec-play'), cover = item.querySelector('.rec-cover'), img = cover.querySelector('img'); + const actions = item.querySelector('.rec-actions'), reason = item.querySelector('.rec-reason'); + const title = item.querySelector('.rec-title'), channel = item.querySelector('.rec-channel'); + const textColor = getComputedStyle(title).color, surface = getComputedStyle(item).backgroundColor; + const shellStyle = getComputedStyle(item); + const actionButtons = [...actions.querySelectorAll('button')].map(button => ({ + rect:rect(button), contrast:contrast(getComputedStyle(button).color, getComputedStyle(button).backgroundColor) + })); + return { + item:rect(item), play:rect(play), cover:rect(cover), image:rect(img), reason:rect(reason), actions:rect(actions), + title:rect(title), channel:rect(channel), scrollWidth:item.scrollWidth, clientWidth:item.clientWidth, + imageFit:getComputedStyle(img).objectFit, titleClamp:getComputedStyle(title).webkitLineClamp, + textColor, surface, reasonColor:getComputedStyle(reason).color, reasonSurface:getComputedStyle(reason).backgroundColor, + shellBorderWidth:parseFloat(shellStyle.borderTopWidth), shellRadius:parseFloat(shellStyle.borderTopLeftRadius), + textContrast:contrast(textColor, surface), reasonContrast:contrast(getComputedStyle(reason).color, getComputedStyle(reason).backgroundColor), + actionButtons, coverRatio:rect(cover).width / rect(cover).height, hasImage:!!img.getAttribute('src'), + fallbackSurface:getComputedStyle(cover).backgroundImage + }; + }); + return { cards, gridColumns:getComputedStyle(section.querySelector('.rec-grid')).gridTemplateColumns }; + }); + matrix.push({ layout, theme, width, ...metrics }); + for (const card of metrics.cards) { + const bottom = card.item.bottom; + for (const [name, box] of Object.entries({ play:card.play, cover:card.cover, title:card.title, channel:card.channel, reason:card.reason, actions:card.actions })) { + if (box.bottom > bottom + 1) failures.push(`${layout}/${theme}/${width}: ${name} spills ${Math.ceil(box.bottom - bottom)}px past rec-item`); + } + if (card.scrollWidth > card.clientWidth + 1) failures.push(`${layout}/${theme}/${width}: rec-item horizontal overflow ${card.scrollWidth}/${card.clientWidth}`); + if (layout === 'glass-stage') { + if (card.shellBorderWidth < 1 || card.shellRadius < 16) failures.push(`${layout}/${theme}/${width}: unified glass shell is missing its rounded border`); + if (card.textContrast < 4.5 || card.reasonContrast < 4.5 || card.actionButtons.some(button => button.contrast < 4.5)) failures.push(`${layout}/${theme}/${width}: text contrast below 4.5:1`); + if (card.actionButtons.some(button => button.rect.width < 44 || button.rect.height < 44)) failures.push(`${layout}/${theme}/${width}: action target below 44px`); + if (card.titleClamp !== '2') failures.push(`${layout}/${theme}/${width}: title clamp is ${card.titleClamp}`); + if (Math.abs(card.coverRatio - 16 / 9) > .03) failures.push(`${layout}/${theme}/${width}: cover aspect ratio ${card.coverRatio}`); + if (card.imageFit !== 'cover') failures.push(`${layout}/${theme}/${width}: thumbnail does not cover-fit`); + if (!card.hasImage && card.fallbackSurface === 'none') failures.push(`${layout}/${theme}/${width}: missing-art fallback has no visual treatment`); + } + } + if (layout === 'glass-stage') { + const focusStyles = await page.locator('#homeRecommendations .rec-play').first().evaluate(button => { + button.focus(); + const playShadow = getComputedStyle(button).boxShadow; + const action = button.closest('.rec-item').querySelector('.rec-actions button'); action.focus(); + return { playShadow, actionShadow:getComputedStyle(action).boxShadow }; + }); + if (focusStyles.playShadow === 'none' || focusStyles.actionShadow === 'none') failures.push(`${layout}/${theme}/${width}: keyboard focus cue missing`); + } + if (width === 390 || width === 1280) { + await page.setViewportSize({ width, height: 2000 }); + await page.evaluate(() => document.activeElement?.blur()); + await page.locator('#homeRecommendations').scrollIntoViewIfNeeded(); + await page.locator('#homeRecommendations .rec-grid').screenshot({ + path: resolve(shots, `${layout}-${theme}-${width}.png`), + style: '.topbar,#bottomNav,#miniBar { display: none !important; }' + }); + } + } + } + console.log(JSON.stringify({ cases:matrix.length, cards:matrix.reduce((n, entry) => n + entry.cards.length, 0), failures, matrix }, null, 2)); + await context.close(); +} finally { await browser.close(); await new Promise(resolveClose => server.close(resolveClose)); } +if (failures.length) process.exitCode = 1; diff --git a/plans/rec-card-shots/bento-hub-contrast-1280.png b/plans/rec-card-shots/bento-hub-contrast-1280.png new file mode 100644 index 0000000..b8a12b3 Binary files /dev/null and b/plans/rec-card-shots/bento-hub-contrast-1280.png differ diff --git a/plans/rec-card-shots/bento-hub-contrast-390.png b/plans/rec-card-shots/bento-hub-contrast-390.png new file mode 100644 index 0000000..572d1ff Binary files /dev/null and b/plans/rec-card-shots/bento-hub-contrast-390.png differ diff --git a/plans/rec-card-shots/bento-hub-dark-1280.png b/plans/rec-card-shots/bento-hub-dark-1280.png new file mode 100644 index 0000000..74b6681 Binary files /dev/null and b/plans/rec-card-shots/bento-hub-dark-1280.png differ diff --git a/plans/rec-card-shots/bento-hub-dark-390.png b/plans/rec-card-shots/bento-hub-dark-390.png new file mode 100644 index 0000000..3e55f9d Binary files /dev/null and b/plans/rec-card-shots/bento-hub-dark-390.png differ diff --git a/plans/rec-card-shots/bento-hub-light-1280.png b/plans/rec-card-shots/bento-hub-light-1280.png new file mode 100644 index 0000000..9047780 Binary files /dev/null and b/plans/rec-card-shots/bento-hub-light-1280.png differ diff --git a/plans/rec-card-shots/bento-hub-light-390.png b/plans/rec-card-shots/bento-hub-light-390.png new file mode 100644 index 0000000..6bf40eb Binary files /dev/null and b/plans/rec-card-shots/bento-hub-light-390.png differ diff --git a/plans/rec-card-shots/classic-contrast-1280.png b/plans/rec-card-shots/classic-contrast-1280.png new file mode 100644 index 0000000..b138e6b Binary files /dev/null and b/plans/rec-card-shots/classic-contrast-1280.png differ diff --git a/plans/rec-card-shots/classic-contrast-390.png b/plans/rec-card-shots/classic-contrast-390.png new file mode 100644 index 0000000..08eaff7 Binary files /dev/null and b/plans/rec-card-shots/classic-contrast-390.png differ diff --git a/plans/rec-card-shots/classic-dark-1280.png b/plans/rec-card-shots/classic-dark-1280.png new file mode 100644 index 0000000..7fda3a1 Binary files /dev/null and b/plans/rec-card-shots/classic-dark-1280.png differ diff --git a/plans/rec-card-shots/classic-dark-390.png b/plans/rec-card-shots/classic-dark-390.png new file mode 100644 index 0000000..3618081 Binary files /dev/null and b/plans/rec-card-shots/classic-dark-390.png differ diff --git a/plans/rec-card-shots/classic-light-1280.png b/plans/rec-card-shots/classic-light-1280.png new file mode 100644 index 0000000..4483919 Binary files /dev/null and b/plans/rec-card-shots/classic-light-1280.png differ diff --git a/plans/rec-card-shots/classic-light-390.png b/plans/rec-card-shots/classic-light-390.png new file mode 100644 index 0000000..2ef2360 Binary files /dev/null and b/plans/rec-card-shots/classic-light-390.png differ diff --git a/plans/rec-card-shots/f7-swipe-contrast-1280.png b/plans/rec-card-shots/f7-swipe-contrast-1280.png new file mode 100644 index 0000000..8735a38 Binary files /dev/null and b/plans/rec-card-shots/f7-swipe-contrast-1280.png differ diff --git a/plans/rec-card-shots/f7-swipe-contrast-390.png b/plans/rec-card-shots/f7-swipe-contrast-390.png new file mode 100644 index 0000000..1627828 Binary files /dev/null and b/plans/rec-card-shots/f7-swipe-contrast-390.png differ diff --git a/plans/rec-card-shots/f7-swipe-dark-1280.png b/plans/rec-card-shots/f7-swipe-dark-1280.png new file mode 100644 index 0000000..516f260 Binary files /dev/null and b/plans/rec-card-shots/f7-swipe-dark-1280.png differ diff --git a/plans/rec-card-shots/f7-swipe-dark-390.png b/plans/rec-card-shots/f7-swipe-dark-390.png new file mode 100644 index 0000000..7989643 Binary files /dev/null and b/plans/rec-card-shots/f7-swipe-dark-390.png differ diff --git a/plans/rec-card-shots/f7-swipe-light-1280.png b/plans/rec-card-shots/f7-swipe-light-1280.png new file mode 100644 index 0000000..cd4ee8f Binary files /dev/null and b/plans/rec-card-shots/f7-swipe-light-1280.png differ diff --git a/plans/rec-card-shots/f7-swipe-light-390.png b/plans/rec-card-shots/f7-swipe-light-390.png new file mode 100644 index 0000000..a9d223f Binary files /dev/null and b/plans/rec-card-shots/f7-swipe-light-390.png differ diff --git a/plans/rec-card-shots/glass-stage-contrast-1280.png b/plans/rec-card-shots/glass-stage-contrast-1280.png new file mode 100644 index 0000000..f864ab9 Binary files /dev/null and b/plans/rec-card-shots/glass-stage-contrast-1280.png differ diff --git a/plans/rec-card-shots/glass-stage-contrast-390.png b/plans/rec-card-shots/glass-stage-contrast-390.png new file mode 100644 index 0000000..69343ab Binary files /dev/null and b/plans/rec-card-shots/glass-stage-contrast-390.png differ diff --git a/plans/rec-card-shots/glass-stage-dark-1280.png b/plans/rec-card-shots/glass-stage-dark-1280.png new file mode 100644 index 0000000..7dd393c Binary files /dev/null and b/plans/rec-card-shots/glass-stage-dark-1280.png differ diff --git a/plans/rec-card-shots/glass-stage-dark-390.png b/plans/rec-card-shots/glass-stage-dark-390.png new file mode 100644 index 0000000..7de7056 Binary files /dev/null and b/plans/rec-card-shots/glass-stage-dark-390.png differ diff --git a/plans/rec-card-shots/glass-stage-light-1280.png b/plans/rec-card-shots/glass-stage-light-1280.png new file mode 100644 index 0000000..5125acb Binary files /dev/null and b/plans/rec-card-shots/glass-stage-light-1280.png differ diff --git a/plans/rec-card-shots/glass-stage-light-390.png b/plans/rec-card-shots/glass-stage-light-390.png new file mode 100644 index 0000000..6f48f95 Binary files /dev/null and b/plans/rec-card-shots/glass-stage-light-390.png differ