prototype: 7 screens + app shell + Faris's review copy - #1
Conversation
Updates prototype/ from the 2026-07-05 six-screen version to current: - content.html: real content detail page (course modules + completion, certificate, video/ebook), replacing the dead-end preview panes - app shell: left sidenav (member nav + partners) with a mobile hamburger drawer; funnel pages keep the topbar - light + dark mode via a single token contract (accent split into text vs fill so it stays legible on both) - Geist / Geist Mono; mono is now scoped to blockchain identifiers only - Feather icon set for content types, nav, and the theme toggle - working buy flow on member-price listings + a personal activity feed - disconnect control; ?demo=fresh|wallet|sub URL state seeding Applies Faris's review (App items 1-4): - hero: 'Single subscription. Multiple benefits.' + his expected subcopy - 'Content' -> 'Packages' - external-link rows now open a real URL (previously Open did nothing) - adds the 'Have premium resources?' partner CTA Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
📝 WalkthroughWalkthroughThe prototype adds persisted progress, ownership, last-viewed, and activity state; introduces content detail interactions; updates dashboard, partner, benefits, subscription, and traction flows; and standardizes navigation, themes, responsive layout, content tiles, and activity presentation. ChangesPrototype feature-flow expansion
Estimated code review effort: 4 (Complex) | ~60 minutes Sequence Diagram(s)sequenceDiagram
participant Member
participant Dashboard
participant ContentPage
participant KState
participant LocalStorage
Member->>Dashboard: Select resume or open content
Dashboard->>KState: Read lastViewed and progress
KState->>LocalStorage: Load persisted member state
Dashboard->>ContentPage: Navigate to content.html
Member->>ContentPage: Complete content action
ContentPage->>KState: Save progress and activity
KState->>LocalStorage: Persist updated state
ContentPage-->>Member: Show updated progress or certificate
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
|
Superseded: pushed directly to main (b64d6f2). prototype/ is ours and touches no packages/ files, so no collision risk. |
There was a problem hiding this comment.
Actionable comments posted: 3
🧹 Nitpick comments (3)
prototype/content.html (1)
8-15: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueInline
<style>block explicitly flagged for promotion.The comment already notes these rules are
candidate for promotiontostyles.css. Worth moving before this pattern is copy-pasted into more pages.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@prototype/content.html` around lines 8 - 15, Move the rules from the inline style block marked “candidate for promotion” into styles.css, preserving the selectors and declarations for .stack, .module-line .sm, `#c-pct`, `#progress-card` .progress, and .media-ph.hero; then remove the inline block from prototype/content.html.prototype/SPEC.md (1)
172-182: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winOriginal "Screen 5: partner.html" section is now stale.
This amendment (and the earlier Mobbin-revisions note) describes partner.html's real, current behavior, but the older "Screen 5" section at Lines 118-126 still documents the superseded design:
06 MEMBER CONTENTlabel, preview-pane toggles, and dummyhref="#"link rows. The PR renames this to "Packages" and wires real URLs (target="_blank"), so the original section now contradicts the implementation and every later amendment. Since this file is the stated build source-of-truth, worth updating/striking the old section rather than layering amendments indefinitely.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@prototype/SPEC.md` around lines 172 - 182, The stale original “Screen 5: partner.html” section should no longer document preview panes, the “06 MEMBER CONTENT” label, or dummy href="#" rows. Replace or remove that section so it describes the current “Packages” layout with real content.html links and unchanged external link rows using target="_blank", consistent with the later partner.html behavior and build source of truth.prototype/styles.css (1)
8-15: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winStylelint violations to clean up (notation/casing/quoting/keyframe naming).
Static analysis flags several convention mismatches introduced in this diff:
@import url(...)should be a bare string (Line 8),SFMono-Regular/Menloshould be lowercase in the--font-monovalue (Line 55),'Geist'shouldn't be quoted in thebodyfont stack (Line 92), and@keyframes screenInshould be kebab-case (Line 776). None of these change behavior, but they'll keep the Stylelint config passing.🧹 Proposed fixes
-@import url('https://fonts.googleapis.com/css2?family=Geist:wght@400;500;600;700;800&family=Geist+Mono:wght@400;700&display=swap'); +@import 'https://fonts.googleapis.com/css2?family=Geist:wght@400;500;600;700;800&family=Geist+Mono:wght@400;700&display=swap';- --font-mono: 'Geist Mono', ui-monospace, SFMono-Regular, Menlo, monospace; + --font-mono: 'Geist Mono', ui-monospace, sfmono-regular, menlo, monospace;- font-family: 'Geist', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; + font-family: Geist, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;-@keyframes screenIn { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: none; } } -.shell { animation: screenIn 320ms cubic-bezier(0.22, 1, 0.36, 1) both; } +@keyframes screen-in { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: none; } } +.shell { animation: screen-in 320ms cubic-bezier(0.22, 1, 0.36, 1) both; }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@prototype/styles.css` around lines 8 - 15, Clean up the Stylelint convention violations in the stylesheet: use a bare string for the Google Fonts `@import`, lowercase the SFMono-Regular and Menlo entries in --font-mono, remove quotes around Geist in the body font stack, and rename the screenIn keyframes identifier to kebab-case while updating all references.Source: Linters/SAST tools
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@prototype/benefits.html`:
- Around line 143-181: Track in-flight purchase indices separately from K.owned,
and update renderListings so pending listings render a disabled “Confirming…”
button instead of an active Buy button. In the rows click handler, mark the
listing pending before starting the timeout, ignore repeated clicks for pending
or owned indices, and clear the pending state when the purchase completes before
re-rendering.
In `@prototype/content.html`:
- Line 86: Update the num-label markup to derive the displayed number from the
current item index or associated data instead of hardcoding “08”; preserve the
existing c-type-label rendering and ensure each rendered content item shows its
correct index.
In `@prototype/styles.css`:
- Around line 718-762: Update the mobile navigation open state around
body.nav-open to lock document scrolling while the drawer is open, preventing
underlying .app-main content from scrolling on touch devices. Preserve normal
page scrolling when nav-open is removed.
---
Nitpick comments:
In `@prototype/content.html`:
- Around line 8-15: Move the rules from the inline style block marked “candidate
for promotion” into styles.css, preserving the selectors and declarations for
.stack, .module-line .sm, `#c-pct`, `#progress-card` .progress, and .media-ph.hero;
then remove the inline block from prototype/content.html.
In `@prototype/SPEC.md`:
- Around line 172-182: The stale original “Screen 5: partner.html” section
should no longer document preview panes, the “06 MEMBER CONTENT” label, or dummy
href="#" rows. Replace or remove that section so it describes the current
“Packages” layout with real content.html links and unchanged external link rows
using target="_blank", consistent with the later partner.html behavior and build
source of truth.
In `@prototype/styles.css`:
- Around line 8-15: Clean up the Stylelint convention violations in the
stylesheet: use a bare string for the Google Fonts `@import`, lowercase the
SFMono-Regular and Menlo entries in --font-mono, remove quotes around Geist in
the body font stack, and rename the screenIn keyframes identifier to kebab-case
while updating all references.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: fae7e079-fc06-4a7e-a911-bc2adead3a8e
⛔ Files ignored due to path filters (7)
prototype/assets/ccl-0.jpgis excluded by!**/*.jpgprototype/assets/ccl-1.jpgis excluded by!**/*.jpgprototype/assets/dwb-0.jpgis excluded by!**/*.jpgprototype/assets/dwb-1.jpgis excluded by!**/*.jpgprototype/assets/hero-community.jpgis excluded by!**/*.jpgprototype/assets/sid-0.jpgis excluded by!**/*.jpgprototype/assets/sid-1.jpgis excluded by!**/*.jpg
📒 Files selected for processing (10)
prototype/SPEC.mdprototype/app.jsprototype/benefits.htmlprototype/content.htmlprototype/dashboard.htmlprototype/index.htmlprototype/partner.htmlprototype/styles.cssprototype/subscribe.htmlprototype/traction.html
| // Listing rows: member price + a working buy flow (owned state persists). | ||
| var rows = document.getElementById("listing-rows"); | ||
| KDATA.listings.forEach(function (l) { | ||
| var row = document.createElement("div"); | ||
| row.className = "benefit-line"; | ||
| row.innerHTML = | ||
| '<div>' + | ||
| '<div class="listing-name">' + l.name + '</div>' + | ||
| '<div class="listing-sub">' + l.kind + ' · ' + l.partner + '</div>' + | ||
| '</div>' + | ||
| '<div class="row tight">' + | ||
| '<span class="price-was">' + fmtXlm(l.priceXlm) + '</span>' + | ||
| '<span class="price-now">' + fmtXlm(l.memberPriceXlm) + '</span>' + | ||
| '</div>'; | ||
| rows.appendChild(row); | ||
| function renderListings() { | ||
| rows.innerHTML = ""; | ||
| var owned = K.owned; | ||
| KDATA.listings.forEach(function (l, i) { | ||
| var row = document.createElement("div"); | ||
| row.className = "benefit-line"; | ||
| var right; | ||
| if (owned.indexOf(i) !== -1) { | ||
| right = '<span class="pill ok">OWNED</span>'; | ||
| } else { | ||
| right = | ||
| '<span class="price-was">' + fmtXlm(l.priceXlm) + '</span>' + | ||
| '<span class="price-now">' + fmtXlm(l.memberPriceXlm) + '</span>' + | ||
| '<button class="ghost sm" data-buy="' + i + '">Buy</button>'; | ||
| } | ||
| row.innerHTML = | ||
| '<div>' + | ||
| '<div class="listing-name">' + l.name + '</div>' + | ||
| '<div class="listing-sub">' + l.kind + ' · ' + l.partner + '</div>' + | ||
| '</div>' + | ||
| '<div class="row tight">' + right + '</div>'; | ||
| rows.appendChild(row); | ||
| }); | ||
| } | ||
| renderListings(); | ||
|
|
||
| rows.addEventListener("click", function (e) { | ||
| var btn = e.target.closest("button[data-buy]"); | ||
| if (!btn) return; | ||
| var i = parseInt(btn.getAttribute("data-buy"), 10); | ||
| btn.disabled = true; | ||
| btn.textContent = "Confirming…"; | ||
| window.setTimeout(function () { | ||
| K.addOwned(i); | ||
| K.logActivity("Purchased", KDATA.listings[i].name + " · " + fmtXlm(KDATA.listings[i].memberPriceXlm)); | ||
| renderListings(); | ||
| }, 1200); |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Concurrent buys clobber each other's "Confirming…" state.
renderListings() rebuilds every row from K.owned only. If a second "Buy" is clicked while a first purchase is still mid-setTimeout, the first purchase's completion re-renders all rows and reverts the second (still-pending) button back to a clickable "Buy" state, since it isn't in K.owned yet. The user can re-click it, and when its original timeout later fires, K.logActivity runs again for the same listing, producing a duplicate "Purchased" entry.
Track in-flight purchases and have renderListings() render a disabled "Confirming…" state for pending indices instead of unconditionally falling back to "Buy".
🛠️ Proposed fix to preserve pending-buy state across re-renders
var rows = document.getElementById("listing-rows");
+ var pendingBuys = {};
function renderListings() {
rows.innerHTML = "";
var owned = K.owned;
KDATA.listings.forEach(function (l, i) {
var row = document.createElement("div");
row.className = "benefit-line";
var right;
if (owned.indexOf(i) !== -1) {
right = '<span class="pill ok">OWNED</span>';
+ } else if (pendingBuys[i]) {
+ right = '<button class="ghost sm" disabled>Confirming…</button>';
} else {
right =
'<span class="price-was">' + fmtXlm(l.priceXlm) + '</span>' +
'<span class="price-now">' + fmtXlm(l.memberPriceXlm) + '</span>' +
'<button class="ghost sm" data-buy="' + i + '">Buy</button>';
}
...
});
}
renderListings();
rows.addEventListener("click", function (e) {
var btn = e.target.closest("button[data-buy]");
if (!btn) return;
var i = parseInt(btn.getAttribute("data-buy"), 10);
+ pendingBuys[i] = true;
btn.disabled = true;
btn.textContent = "Confirming…";
window.setTimeout(function () {
K.addOwned(i);
K.logActivity("Purchased", KDATA.listings[i].name + " · " + fmtXlm(KDATA.listings[i].memberPriceXlm));
+ delete pendingBuys[i];
renderListings();
}, 1200);
});📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| // Listing rows: member price + a working buy flow (owned state persists). | |
| var rows = document.getElementById("listing-rows"); | |
| KDATA.listings.forEach(function (l) { | |
| var row = document.createElement("div"); | |
| row.className = "benefit-line"; | |
| row.innerHTML = | |
| '<div>' + | |
| '<div class="listing-name">' + l.name + '</div>' + | |
| '<div class="listing-sub">' + l.kind + ' · ' + l.partner + '</div>' + | |
| '</div>' + | |
| '<div class="row tight">' + | |
| '<span class="price-was">' + fmtXlm(l.priceXlm) + '</span>' + | |
| '<span class="price-now">' + fmtXlm(l.memberPriceXlm) + '</span>' + | |
| '</div>'; | |
| rows.appendChild(row); | |
| function renderListings() { | |
| rows.innerHTML = ""; | |
| var owned = K.owned; | |
| KDATA.listings.forEach(function (l, i) { | |
| var row = document.createElement("div"); | |
| row.className = "benefit-line"; | |
| var right; | |
| if (owned.indexOf(i) !== -1) { | |
| right = '<span class="pill ok">OWNED</span>'; | |
| } else { | |
| right = | |
| '<span class="price-was">' + fmtXlm(l.priceXlm) + '</span>' + | |
| '<span class="price-now">' + fmtXlm(l.memberPriceXlm) + '</span>' + | |
| '<button class="ghost sm" data-buy="' + i + '">Buy</button>'; | |
| } | |
| row.innerHTML = | |
| '<div>' + | |
| '<div class="listing-name">' + l.name + '</div>' + | |
| '<div class="listing-sub">' + l.kind + ' · ' + l.partner + '</div>' + | |
| '</div>' + | |
| '<div class="row tight">' + right + '</div>'; | |
| rows.appendChild(row); | |
| }); | |
| } | |
| renderListings(); | |
| rows.addEventListener("click", function (e) { | |
| var btn = e.target.closest("button[data-buy]"); | |
| if (!btn) return; | |
| var i = parseInt(btn.getAttribute("data-buy"), 10); | |
| btn.disabled = true; | |
| btn.textContent = "Confirming…"; | |
| window.setTimeout(function () { | |
| K.addOwned(i); | |
| K.logActivity("Purchased", KDATA.listings[i].name + " · " + fmtXlm(KDATA.listings[i].memberPriceXlm)); | |
| renderListings(); | |
| }, 1200); | |
| // Listing rows: member price + a working buy flow (owned state persists). | |
| var rows = document.getElementById("listing-rows"); | |
| var pendingBuys = {}; | |
| function renderListings() { | |
| rows.innerHTML = ""; | |
| var owned = K.owned; | |
| KDATA.listings.forEach(function (l, i) { | |
| var row = document.createElement("div"); | |
| row.className = "benefit-line"; | |
| var right; | |
| if (owned.indexOf(i) !== -1) { | |
| right = '<span class="pill ok">OWNED</span>'; | |
| } else if (pendingBuys[i]) { | |
| right = '<button class="ghost sm" disabled>Confirming…</button>'; | |
| } else { | |
| right = | |
| '<span class="price-was">' + fmtXlm(l.priceXlm) + '</span>' + | |
| '<span class="price-now">' + fmtXlm(l.memberPriceXlm) + '</span>' + | |
| '<button class="ghost sm" data-buy="' + i + '">Buy</button>'; | |
| } | |
| row.innerHTML = | |
| '<div>' + | |
| '<div class="listing-name">' + l.name + '</div>' + | |
| '<div class="listing-sub">' + l.kind + ' · ' + l.partner + '</div>' + | |
| '</div>' + | |
| '<div class="row tight">' + right + '</div>'; | |
| rows.appendChild(row); | |
| }); | |
| } | |
| renderListings(); | |
| rows.addEventListener("click", function (e) { | |
| var btn = e.target.closest("button[data-buy]"); | |
| if (!btn) return; | |
| var i = parseInt(btn.getAttribute("data-buy"), 10); | |
| pendingBuys[i] = true; | |
| btn.disabled = true; | |
| btn.textContent = "Confirming…"; | |
| window.setTimeout(function () { | |
| K.addOwned(i); | |
| K.logActivity("Purchased", KDATA.listings[i].name + " · " + fmtXlm(KDATA.listings[i].memberPriceXlm)); | |
| delete pendingBuys[i]; | |
| renderListings(); | |
| }, 1200); | |
| }); |
🧰 Tools
🪛 ast-grep (0.44.1)
[warning] 159-164: Avoid assigning untrusted data to innerHTML/outerHTML or document.write
Context: row.innerHTML =
'
'
'
'
'
Note: [CWE-79] Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting').
(inner-outer-html)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@prototype/benefits.html` around lines 143 - 181, Track in-flight purchase
indices separately from K.owned, and update renderListings so pending listings
render a disabled “Confirming…” button instead of an active Buy button. In the
rows click handler, mark the listing pending before starting the timeout, ignore
repeated clicks for pending or owned indices, and clear the pending state when
the purchase completes before re-rendering.
|
|
||
| <div class="card"> | ||
| <div class="thumb hero" id="c-thumb"></div> | ||
| <div class="num-label"><span class="num">08</span> <span id="c-type-label"></span></div> |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Hardcoded "08" label doesn't reflect the displayed content.
<span class="num">08</span> is static regardless of which content item/type is rendered, so every content page shows "08 COURSE"/"08 VIDEO"/"08 EBOOK". If this is meant to be a per-item index, it should be derived from idx/data rather than hardcoded.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@prototype/content.html` at line 86, Update the num-label markup to derive the
displayed number from the current item index or associated data instead of
hardcoding “08”; preserve the existing c-type-label rendering and ensure each
rendered content item shows its correct index.
| @media (max-width: 900px) { | ||
| .grid-2, .grid-3, .grid-dash, .grid-detail { grid-template-columns: 1fr; } | ||
| .grid-dash .stat-strip { flex-direction: row; } | ||
|
|
||
| /* sidenav becomes an off-canvas drawer */ | ||
| .sidenav { | ||
| position: fixed; | ||
| top: 0; | ||
| left: 0; | ||
| bottom: 0; | ||
| width: 264px; | ||
| height: 100vh; | ||
| transform: translateX(-100%); | ||
| transition: transform 200ms ease; | ||
| z-index: 50; | ||
| background: var(--color-bg-primary); | ||
| border-right: 1px solid var(--color-border-medium); | ||
| } | ||
| body.nav-open .sidenav { transform: translateX(0); } | ||
|
|
||
| .nav-overlay { | ||
| display: block; | ||
| position: fixed; | ||
| inset: 0; | ||
| background: rgba(0, 0, 0, 0.5); | ||
| opacity: 0; | ||
| pointer-events: none; | ||
| transition: opacity 200ms ease; | ||
| z-index: 40; | ||
| } | ||
| body.nav-open .nav-overlay { opacity: 1; pointer-events: auto; } | ||
|
|
||
| .mobile-topbar { | ||
| display: flex; | ||
| align-items: center; | ||
| gap: var(--space-3); | ||
| padding: var(--space-3) var(--space-4); | ||
| border-bottom: 1px solid var(--color-border-medium); | ||
| position: sticky; | ||
| top: 0; | ||
| z-index: 30; | ||
| background: var(--color-bg-primary); | ||
| } | ||
| .mobile-topbar .logo { font-size: 18px; } | ||
| } |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Background scrolls behind the open mobile drawer.
body.nav-open toggles the .sidenav/.nav-overlay visuals but never locks page scroll. On a touch device, a user can still scroll the underlying .app-main content while the drawer/overlay is open.
🔧 Proposed fix
body.nav-open .sidenav { transform: translateX(0); }
+ body.nav-open { overflow: hidden; }📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| @media (max-width: 900px) { | |
| .grid-2, .grid-3, .grid-dash, .grid-detail { grid-template-columns: 1fr; } | |
| .grid-dash .stat-strip { flex-direction: row; } | |
| /* sidenav becomes an off-canvas drawer */ | |
| .sidenav { | |
| position: fixed; | |
| top: 0; | |
| left: 0; | |
| bottom: 0; | |
| width: 264px; | |
| height: 100vh; | |
| transform: translateX(-100%); | |
| transition: transform 200ms ease; | |
| z-index: 50; | |
| background: var(--color-bg-primary); | |
| border-right: 1px solid var(--color-border-medium); | |
| } | |
| body.nav-open .sidenav { transform: translateX(0); } | |
| .nav-overlay { | |
| display: block; | |
| position: fixed; | |
| inset: 0; | |
| background: rgba(0, 0, 0, 0.5); | |
| opacity: 0; | |
| pointer-events: none; | |
| transition: opacity 200ms ease; | |
| z-index: 40; | |
| } | |
| body.nav-open .nav-overlay { opacity: 1; pointer-events: auto; } | |
| .mobile-topbar { | |
| display: flex; | |
| align-items: center; | |
| gap: var(--space-3); | |
| padding: var(--space-3) var(--space-4); | |
| border-bottom: 1px solid var(--color-border-medium); | |
| position: sticky; | |
| top: 0; | |
| z-index: 30; | |
| background: var(--color-bg-primary); | |
| } | |
| .mobile-topbar .logo { font-size: 18px; } | |
| } | |
| `@media` (max-width: 900px) { | |
| .grid-2, .grid-3, .grid-dash, .grid-detail { grid-template-columns: 1fr; } | |
| .grid-dash .stat-strip { flex-direction: row; } | |
| /* sidenav becomes an off-canvas drawer */ | |
| .sidenav { | |
| position: fixed; | |
| top: 0; | |
| left: 0; | |
| bottom: 0; | |
| width: 264px; | |
| height: 100vh; | |
| transform: translateX(-100%); | |
| transition: transform 200ms ease; | |
| z-index: 50; | |
| background: var(--color-bg-primary); | |
| border-right: 1px solid var(--color-border-medium); | |
| } | |
| body.nav-open .sidenav { transform: translateX(0); } | |
| body.nav-open { overflow: hidden; } | |
| .nav-overlay { | |
| display: block; | |
| position: fixed; | |
| inset: 0; | |
| background: rgba(0, 0, 0, 0.5); | |
| opacity: 0; | |
| pointer-events: none; | |
| transition: opacity 200ms ease; | |
| z-index: 40; | |
| } | |
| body.nav-open .nav-overlay { opacity: 1; pointer-events: auto; } | |
| .mobile-topbar { | |
| display: flex; | |
| align-items: center; | |
| gap: var(--space-3); | |
| padding: var(--space-3) var(--space-4); | |
| border-bottom: 1px solid var(--color-border-medium); | |
| position: sticky; | |
| top: 0; | |
| z-index: 30; | |
| background: var(--color-bg-primary); | |
| } | |
| .mobile-topbar .logo { font-size: 18px; } | |
| } |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@prototype/styles.css` around lines 718 - 762, Update the mobile navigation
open state around body.nav-open to lock document scrolling while the drawer is
open, preventing underlying .app-main content from scrolling on touch devices.
Preserve normal page scrolling when nav-open is removed.
Updates
prototype/from the 2026-07-05 six-screen version to current. Only touchesprototype/— nopackages/files, so it should not collide with the app work.Applies Faris's review (App items 1–4)
What else is new since 07-05
content.html: real content detail page — course modules with completion, certificate state, video/ebook views. Replaces the old dead-end preview panes.?demo=fresh|wallet|subURL state seeding (handy for demo capture and the demo video).prototype/SPEC.mddocuments all of it.🤖 Generated with Claude Code
Summary by CodeRabbit
New Features
Style