';
intro.querySelector('.ai-upgrade-btn')?.addEventListener('click',()=>$('pricing-modal')?.classList.remove('hidden'));
}else if(isAuthErr){
diff --git a/features-init.js b/features-init.js
new file mode 100644
index 0000000..b77e36c
--- /dev/null
+++ b/features-init.js
@@ -0,0 +1,40 @@
+// Features page motion: reveal sections as they arrive, draw the curve once it is seen, let the
+// product frame breathe and tilt under the pointer, and let each card's glow follow the mouse.
+// Everything here is decorative; the page reads the same with scripting off or reduced motion.
+(() => {
+ 'use strict';
+ const reduce = matchMedia('(prefers-reduced-motion: reduce)').matches;
+ const reveals = [...document.querySelectorAll('.fx-reveal')];
+ if (reduce || !('IntersectionObserver' in window)) reveals.forEach(el => el.classList.add('is-in'));
+ else {
+ const io = new IntersectionObserver(entries => {
+ entries.forEach(entry => { if (entry.isIntersecting) { entry.target.classList.add('is-in'); io.unobserve(entry.target); } });
+ }, { rootMargin: '0px 0px -10% 0px', threshold: 0.15 });
+ reveals.forEach((el, i) => { el.style.transitionDelay = (i % 3) * 90 + 'ms'; io.observe(el); });
+ }
+ if (reduce) return;
+
+ // The frame tilts a few degrees toward the pointer and settles back when it leaves.
+ const frame = document.getElementById('fx-frame');
+ const win = frame && frame.querySelector('.fx-window');
+ if (win && matchMedia('(hover: hover)').matches) {
+ let raf = 0, rx = 0, ry = 0;
+ const paint = () => { raf = 0; win.style.transform = 'rotateX(' + rx.toFixed(2) + 'deg) rotateY(' + ry.toFixed(2) + 'deg) scale(1.01)'; };
+ frame.addEventListener('pointermove', e => {
+ const r = win.getBoundingClientRect();
+ const px = (e.clientX - r.left) / r.width - 0.5, py = (e.clientY - r.top) / r.height - 0.5;
+ rx = -py * 5; ry = px * 6;
+ if (!raf) raf = requestAnimationFrame(paint);
+ });
+ frame.addEventListener('pointerleave', () => { if (raf) cancelAnimationFrame(raf); raf = 0; win.style.transform = ''; });
+ }
+
+ // Each card's warm glow follows the pointer.
+ document.querySelectorAll('.fx-card').forEach(card => {
+ card.addEventListener('pointermove', e => {
+ const r = card.getBoundingClientRect();
+ card.style.setProperty('--mx', ((e.clientX - r.left) / r.width * 100).toFixed(1) + '%');
+ card.style.setProperty('--my', ((e.clientY - r.top) / r.height * 100).toFixed(1) + '%');
+ });
+ });
+})();
diff --git a/features.css b/features.css
new file mode 100644
index 0000000..445af72
--- /dev/null
+++ b/features.css
@@ -0,0 +1,134 @@
+/* Features page. Tokens come from landing.css (dark by default, light under [data-theme=light]).
+ Motion: cards lift and warm on hover, the product frame breathes and tilts under the pointer,
+ the curve draws itself in when it scrolls into view. All of it is off under reduced motion. */
+.fx{--ease:cubic-bezier(.2,.8,.2,1);--lift:0 28px 60px -32px rgba(0,0,0,.65);--glow:0 0 0 1px var(--gold)}
+.fx-body main{overflow-x:clip}
+.fx h1,.fx h2,.fx h3{font-family:var(--display);font-weight:600;line-height:1.08;color:var(--text);margin:0}
+.fx h1{font-size:clamp(2.3rem,4.3vw,3.5rem);letter-spacing:-.01em;max-width:15ch}
+.fx h1 em,.fx h2 em{font-style:italic;color:var(--gold)}
+.fx h2{font-size:clamp(1.9rem,3.6vw,2.9rem)}
+.fx h3{font-size:1.35rem}
+.fx p{color:var(--muted);line-height:1.6}
+.fx .eyebrow{display:block;margin-bottom:var(--space-4)}
+.fx-lede{font-size:1.05rem;max-width:44ch;margin:var(--space-6) 0 var(--space-8)}
+
+/* Buttons on the dark surfaces: the homepage's outline style was drawn for its paper hero. */
+.fx .button{margin:0}
+.fx .button.outline{background:transparent;color:var(--text);border:1px solid color-mix(in srgb,var(--gold) 60%,var(--line));box-shadow:none;transition:border-color .4s var(--ease),background .4s var(--ease),transform .4s var(--ease)}
+.fx .button.outline:hover{border-color:var(--gold);background:color-mix(in srgb,var(--gold) 10%,transparent);transform:translateY(-2px)}
+
+/* Hero */
+.fx-hero{display:grid;grid-template-columns:minmax(0,5fr) minmax(0,7fr);gap:clamp(2rem,5vw,5rem);align-items:center;padding-top:clamp(1rem,4vw,3rem);padding-bottom:clamp(2rem,4vw,3rem)}
+.fx-hero .hero-actions{display:flex;flex-wrap:wrap;justify-content:flex-start;gap:var(--space-3);margin:0}
+.fx-hero .hero-note{margin-top:var(--space-4);font-size:var(--text-sm);text-align:left}
+
+/* The product frame: the workspace as it renders, built from real output */
+.fx-frame{margin:0;perspective:1400px}
+.fx-window{background:var(--surface);border:1px solid var(--line);border-radius:14px;box-shadow:0 40px 90px -40px rgba(0,0,0,.8),0 0 0 1px rgba(0,0,0,.2);transform-style:preserve-3d;transition:transform .6s var(--ease),box-shadow .6s var(--ease);will-change:transform}
+.fx-frame:hover .fx-window{box-shadow:0 60px 110px -40px rgba(0,0,0,.85),var(--glow)}
+.fx-window-bar{display:flex;justify-content:space-between;align-items:baseline;gap:var(--space-4);padding:.7rem 1rem;border-bottom:1px solid var(--line);font-size:.72rem;color:var(--muted)}
+.fx-window-title{font-family:var(--display);font-size:1.05rem;color:var(--text)}
+.fx-window-body{display:grid;grid-template-columns:150px minmax(0,1fr)}
+.fx-window-nav{display:flex;flex-direction:column;gap:.15rem;padding:.8rem .6rem;border-right:1px solid var(--line);font-size:.7rem;color:var(--muted)}
+.fx-window-nav span{padding:.35rem .55rem;border-radius:6px}
+.fx-window-nav .on{background:color-mix(in srgb,var(--gold) 14%,transparent);color:var(--text)}
+.fx-panel{padding:1rem 1.1rem 1.1rem;min-width:0}
+.fx-panel-head{display:flex;justify-content:space-between;align-items:baseline;gap:var(--space-3)}
+.fx-panel-head h2{font-size:1.1rem}
+.fx-panel-head span{font-size:.66rem;color:var(--muted)}
+.fx-panel-lede{font-size:.74rem;margin:.35rem 0 .5rem}
+.fx-curve{display:block;width:100%;height:auto}
+.fx-grid{stroke:var(--line);stroke-width:1}
+.fx-tick,.fx-axis text{fill:var(--muted);font-size:9px;font-family:var(--body)}
+.fx-axis-label{letter-spacing:.08em;font-size:8px}
+.fx-area{fill:var(--gold);opacity:.14}
+.fx-line{fill:none;stroke:var(--gold);stroke-width:2;stroke-linejoin:round;stroke-linecap:round;stroke-dasharray:700;stroke-dashoffset:700}
+.fx-dots circle{fill:var(--surface);stroke:var(--gold);stroke-width:1.5;opacity:0;transition:r .3s var(--ease)}
+.fx-dots .peak{fill:var(--gold)}
+.fx-dots circle:hover{r:6}
+.fx-frame.is-in .fx-line{animation:fx-draw 1.6s var(--ease) forwards}
+.fx-frame.is-in .fx-dots circle{animation:fx-appear .5s var(--ease) forwards}
+.fx-frame.is-in .fx-dots circle:nth-child(1){animation-delay:.2s}.fx-frame.is-in .fx-dots circle:nth-child(2){animation-delay:.4s}.fx-frame.is-in .fx-dots circle:nth-child(3){animation-delay:.6s}.fx-frame.is-in .fx-dots circle:nth-child(4){animation-delay:.8s}.fx-frame.is-in .fx-dots circle:nth-child(5){animation-delay:1s}.fx-frame.is-in .fx-dots circle:nth-child(6){animation-delay:1.2s}.fx-frame.is-in .fx-dots circle:nth-child(7){animation-delay:1.4s}.fx-frame.is-in .fx-dots circle:nth-child(8){animation-delay:1.6s}
+@keyframes fx-draw{to{stroke-dashoffset:0}}
+@keyframes fx-appear{to{opacity:1}}
+.fx-curve-label{font-size:.66rem;text-align:center;margin:.1rem 0 .7rem}
+.fx-finding{display:flex;justify-content:space-between;align-items:center;gap:var(--space-4);background:color-mix(in srgb,var(--gold) 8%,transparent);border:1px solid var(--line);border-left:3px solid var(--gold);border-radius:10px;padding:.65rem .8rem}
+.fx-finding h3{font-family:var(--body);font-weight:600;font-size:.78rem;line-height:1.35}
+.fx-finding p{font-size:.7rem;margin:.15rem 0 0}
+.fx-inspect{flex-shrink:0;font-size:.7rem;color:var(--gold);border:1px solid var(--line);border-radius:8px;padding:.35rem .6rem;text-decoration:none;transition:background .3s var(--ease),transform .3s var(--ease)}
+.fx-inspect:hover{background:color-mix(in srgb,var(--gold) 16%,transparent);transform:translateX(2px)}
+.fx-frame figcaption{margin-top:var(--space-3);font-size:var(--text-xs);color:var(--muted);text-align:center}
+
+/* Sections */
+.fx-section{padding:clamp(3rem,7vw,6rem) clamp(1.25rem,5vw,5rem)}
+.fx-section-intro{max-width:72ch;margin:0 auto var(--space-12);text-align:center}
+.fx-section-intro p{margin-top:var(--space-4)}
+.fx-columns{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:var(--space-6)}
+
+/* Cards: the breathing surface */
+.fx-card{position:relative;background:var(--surface);border:1px solid var(--line);border-radius:16px;padding:var(--space-8);transition:transform .55s var(--ease),box-shadow .55s var(--ease),border-color .55s var(--ease)}
+.fx-card::after{content:"";position:absolute;inset:0;border-radius:inherit;background:radial-gradient(600px circle at var(--mx,50%) var(--my,0%),color-mix(in srgb,var(--gold) 12%,transparent),transparent 45%);opacity:0;transition:opacity .55s var(--ease);pointer-events:none}
+.fx-card:hover{transform:translateY(-6px);box-shadow:var(--lift);border-color:color-mix(in srgb,var(--gold) 55%,var(--line))}
+.fx-card:hover::after{opacity:1}
+.fx-icon{width:28px;height:28px;fill:none;stroke:var(--gold);stroke-width:1.5;stroke-linecap:round;stroke-linejoin:round;margin-bottom:var(--space-4);transition:transform .55s var(--ease)}
+.fx-card:hover .fx-icon{transform:translateY(-2px) scale(1.06)}
+.fx-kicker{display:block;font-size:var(--text-xs);letter-spacing:.14em;text-transform:uppercase;color:var(--gold);margin-bottom:var(--space-3)}
+.fx-card h3{margin-bottom:var(--space-3)}
+.fx-card>p{font-size:var(--text-sm)}
+.fx-list{list-style:none;margin:var(--space-6) 0 0;padding:0;display:grid;gap:.55rem}
+.fx-list li{position:relative;padding-left:1.7rem;font-size:var(--text-sm);color:var(--text)}
+.fx-list li::before{content:"";position:absolute;left:0;top:.2em;width:.95rem;height:.95rem;border:1.5px solid var(--gold);border-radius:50%;opacity:.7;transition:opacity .4s var(--ease),background .4s var(--ease)}
+.fx-list li::after{content:"";position:absolute;left:.29rem;top:.42em;width:.28rem;height:.5rem;border:solid var(--gold);border-width:0 1.5px 1.5px 0;transform:rotate(45deg);opacity:.7;transition:opacity .4s var(--ease),border-color .4s var(--ease)}
+.fx-card:hover .fx-list li::before{opacity:1;background:color-mix(in srgb,var(--gold) 12%,transparent)}
+.fx-card:hover .fx-list li::after{opacity:1}
+
+/* Book Intelligence */
+.fx-intel{display:grid;grid-template-columns:minmax(0,5fr) minmax(0,7fr);gap:clamp(2rem,5vw,5rem);align-items:center;border-top:1px solid var(--line);border-bottom:1px solid var(--line);background:linear-gradient(180deg,color-mix(in srgb,var(--gold) 4%,transparent),transparent)}
+.fx-intel-copy p{margin:var(--space-4) 0 var(--space-6)}
+.fx-inline-list{list-style:none;margin:0 0 var(--space-8);padding:0;display:flex;flex-wrap:wrap;gap:.4rem .9rem;font-size:var(--text-sm);color:var(--text)}
+.fx-inline-list li::before{content:"·";color:var(--gold);margin-right:.5rem}
+.fx-intel-demo{display:grid;grid-template-columns:minmax(0,1fr) minmax(0,1fr);gap:var(--space-4)}
+.fx-excerpt p{font-family:var(--display);font-size:1.15rem;line-height:1.5;color:var(--text);margin:0 0 .8rem}
+.fx-excerpt mark{background:color-mix(in srgb,var(--gold) 22%,transparent);color:inherit;padding:0 .15em;border-bottom:1px solid var(--gold)}
+.fx-insight h3{font-size:1.1rem;margin-bottom:var(--space-3)}
+.fx-insight p{font-size:var(--text-sm)}
+.fx-note{display:block;margin-top:var(--space-4);font-size:var(--text-xs);color:var(--muted)}
+
+/* Genres */
+.fx-genres{display:grid;grid-template-columns:minmax(0,5fr) minmax(0,7fr);gap:clamp(2rem,5vw,5rem);align-items:center}
+.fx-genre-copy p{margin-top:var(--space-4)}
+.fx-chips{list-style:none;margin:0;padding:0;display:flex;flex-wrap:wrap;gap:.6rem}
+.fx-chips li{border:1px solid var(--line);border-radius:999px;padding:.5rem 1rem;font-size:var(--text-sm);color:var(--text);background:var(--surface);transition:transform .45s var(--ease),border-color .45s var(--ease),background .45s var(--ease)}
+.fx-chips li.nf{border-style:dashed}
+.fx-chips li:hover{transform:translateY(-3px);border-color:var(--gold);background:color-mix(in srgb,var(--gold) 10%,var(--surface))}
+
+/* Close */
+.fx-close{text-align:center;padding-top:clamp(3rem,7vw,6rem);padding-bottom:clamp(3rem,7vw,6rem)}
+.fx-close h2{margin-bottom:var(--space-8)}
+.fx-close .hero-note{margin-top:var(--space-4)}
+
+/* Reveal on scroll */
+.fx-reveal{opacity:0;transform:translateY(18px);transition:opacity .9s var(--ease),transform .9s var(--ease)}
+.fx-reveal.is-in{opacity:1;transform:none}
+.fx-card.fx-reveal.is-in{transform:none}
+.fx-card.fx-reveal.is-in:hover{transform:translateY(-6px)}
+
+@media(max-width:960px){
+ .fx-hero,.fx-intel,.fx-genres{grid-template-columns:1fr}
+ .fx-columns{grid-template-columns:1fr}
+ .fx h1{max-width:none}
+ .fx-window-body{grid-template-columns:1fr}
+ .fx-window-nav{display:none}
+}
+@media(max-width:600px){
+ .fx-intel-demo{grid-template-columns:1fr}
+ .fx-finding{flex-direction:column;align-items:flex-start}
+ .fx-card{padding:var(--space-6)}
+}
+@media(prefers-reduced-motion:reduce){
+ .fx-reveal{opacity:1;transform:none;transition:none}
+ .fx-line{stroke-dashoffset:0;animation:none!important}
+ .fx-dots circle{opacity:1;animation:none!important}
+ .fx-card,.fx-chips li,.fx-window,.fx-icon,.fx-inspect{transition:none}
+ .fx-card:hover,.fx-chips li:hover{transform:none}
+}
diff --git a/features.html b/features.html
index c5c165a..66833ee 100644
--- a/features.html
+++ b/features.html
@@ -1,275 +1,205 @@
-
-
-
-AuthorScrolls — Product Features
-
-
+
+
+
+
+ AuthorScrolls | Features
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
Product Features
-
The manuscript diagnostic built for serious authors.
-
Not a grammar checker. Not a spell-checker. A structural engine that reads your manuscript the way an editor does — and tells you exactly what isn't working, before you pay someone else to find it.
The analysis engine runs entirely in your browser. Nothing is sent to a server until you choose to use AI features. Your manuscript stays private.
-
-
-
-
Free
-
🚫
-
DNF Prediction Engine
-
Scores your manuscript on six reader-dropout dimensions: hook strength, clarity, forward motion, specificity, redundancy, and payoff. Tells you which section loses readers first and exactly why.
-
-
-
Free
-
📈
-
Plot & Structure
-
Detects your story arc across four quarters of the manuscript. Rising action, climax, resolution, and cliffhanger patterns scored and mapped. Flags a sagging middle before you submit.
-
-
-
Free
-
⏬
-
Pacing Heatmap
-
Visualizes the balance of action, dialogue, description, exposition, and reflection across your manuscript. Spots where your pacing stalls or rushes.
-
-
-
Free
-
💬
-
Dialogue Analysis
-
Scores tag discipline, conciseness, naturalness, and show-not-tell within dialogue. Flags exposition dumps, repetitive speech patterns, and over-attribution.
-
-
-
Free
-
👁
-
Show vs. Tell Detection
-
Maps 32 emotions to their physical manifestations. Flags "she felt angry" and suggests "jaw tightened, hands clenched, voice sharpened." Not a generic rule — a specific replacement.
-
-
-
Free
-
✍
-
Line Editing Score
-
Six sub-scores: tone consistency, flow, word precision, sentence pacing, POV discipline, and extraneous language. Each scored independently, each improvable.
-
-
-
Free
-
🔢
-
Copy Editing Detection
-
Passive voice with active-voice rewrites. Adverbs mapped to stronger verbs. 90+ clichés flagged. Weak verbs replaced. Wordy phrases trimmed. All with specific suggestions, not just flags.
-
-
-
Free
-
✏
-
Grammar & Typos
-
2,000+ grammar rules via LanguageTool: comma splices, run-ons, subject-verb agreement, confused words, tense shifts, dialogue punctuation. Proper nouns and invented names filtered to avoid false flags.
-
-
-
Free
-
🎯
-
Opening Diagnosis
-
Your first pages scored on hook strength, tension, specificity, clarity, forward motion, and genre-matching. Tells you what a cold reader — or agent — encounters in the first 500 words.
-
-
-
-
-
◆
-
-
-
-
AI Features — Paid Plans
-
Deeper analysis when you’re close to ready.
-
AI features send your manuscript to language models for analysis that goes beyond pattern matching. Used when you’re actively revising or preparing to query.
-
-
-
-
Starter & Premium
-
⚡
-
Deep Narrative Critique
-
A full developmental editorial pass: character arc, stakes, conflict engine, voice consistency, pacing at the chapter level. The kind of feedback a developmental editor gives at $2,000+. Delivered in one click.
-
-
-
Starter & Premium
-
🔎
-
Weak Passage Diagnosis
-
Identifies your three weakest passages by paragraph, explains exactly why each one loses reader momentum, and suggests a concrete rewrite. Specific text from your manuscript, not generic advice.
-
-
-
Starter & Premium
-
📖
-
Query Letter Draft
-
A query letter scaffold drawn from your manuscript’s actual story beats, genre, word count, and themes. Not a template with blanks to fill — a draft built from what’s in your book.
-
-
-
Starter & Premium
-
📚
-
Comparable Titles
-
Identifies comparable published titles based on genre, themes, voice, and structural patterns in your manuscript. Gives you the comp titles agents ask for, grounded in what you actually wrote.
-
-
-
Premium
-
👥
-
Beta Reader Simulation
-
Five distinct reader personas respond to your manuscript: the casual reader, the genre purist, the literary reader, the agent, and the skeptic. Surface problems that only real readers would catch.
-
-
-
Premium
-
📊
-
Market Readiness Score
-
An honest assessment of where your manuscript stands against current publishing standards for your genre. Not a vanity score — a gap analysis between where you are and where you need to be.
-
-
-
-
-
◆
-
-
-
-
Genre Coverage
-
19 genres. Each judged on its own terms.
-
Genre context changes what good writing looks like. A thriller opening is scored on tension and pace. A self-help introduction is scored on authority and clarity of promise. The engine knows the difference.
Nonfiction scoring weights authority establishment, structural clarity, reader promise, and argument momentum differently from fiction’s tension and character arcs.
-
-
-
-
-
◆
-
-
-
-
-
What AuthorScrolls doesn’t do — and won’t claim to.
-
This tool does not replace a copy editor, line editor, or proofreader. It does not guarantee publication. It does not rewrite your book. A developmental editor, a copy editor, and a proofreader are still part of the path to a finished, publishable manuscript.
What AuthorScrolls does: finds the structural, stylistic, and craft problems you can fix yourself — before you spend money on professional editors to find them for you. Most writers go through two or three rounds of expensive editing before the core problems are addressed. This is the step that changes that.
-
-
-
-
-
-
Start with your manuscript.
-
No credit card. No limit on how many times you analyze. The full diagnostic engine is free.
The manuscript diagnostic built for serious authors.
+
Understand how your story is built, find the writing that gets in its way, and clean the manuscript, with an engine that reads the whole thing and shows you where it looked.
+ Real output on a public-domain novel. Open it yourself in the workspace demo.
+
+
+
+
+
+
+ CORE ANALYSIS · FREE
+
A complete diagnostic, in minutes.
+
Upload a Word document, PDF or text file and get a detailed reading across structure, craft and mechanics. Every score shows the counts it was built from, and anything the engine cannot measure is described rather than scored.
+
+
+
+
+ Structure
+
Understand the shape of your story.
+
How much of each chapter is scene against summary, where that thins or spikes, chapter length against the book’s own norm, and whether the cast persists.
+
+
Narrative structure, described with the chapters to inspect
+
Pacing heatmap by kind of prose
+
Opening diagnosis
+
Reader-momentum signals
+
+
+
+
+ Craft
+
Find the writing that’s getting in the way.
+
Judged for the passage it sits in. A long sentence in a reflective stretch is measured against your other reflective stretches, not a fixed number.
The air tasted like ash, and Narubi watched the skyline burn. She had survived worse, but this felt different.
+
It wasn’t just a city falling.
+
It was a choice.
+
+
+ Book Intelligence · advisory
+
The emotion is summarised where the scene could carry it
+
“This felt different” names the feeling instead of letting the reader find it. The image before it, ash and a burning skyline, is doing the work; consider what Narubi does with her hands, her breath, her attention.
+ Interpretation, not measurement. Your scores are unchanged.
+
+
+
+
+
+
+
+ BUILT FOR YOUR STORY
+
20 genres. Analysis that understands the difference.
+
Genre changes what good writing looks like. A self-help book is not judged against fiction goals, a thriller is not asked to slow down, and the left rail tells you what a reader of your genre comes for.
+
+
+
Science Fiction
Fantasy
Romance
Thriller
Mystery
Horror
Historical
Dystopian
Young Adult
Literary Fiction
Romantasy
Cozy Mystery
Adventure
Western
+
Memoir
Self-Help
Biography
History
True Crime
Philosophy
+
+
+
+
+
+
Your manuscript already contains the evidence. See what it says.
+
+
No credit card. Your manuscript stays private.
+
+
+
+
+
+
diff --git a/scripts/browser-smoke.js b/scripts/browser-smoke.js
index 2b27a8f..e0c6d9b 100644
--- a/scripts/browser-smoke.js
+++ b/scripts/browser-smoke.js
@@ -381,6 +381,23 @@ function pdf(){
await page.locator('#auth-dialog[open]').waitFor();
await page.keyboard.press('Escape');
console.log('PASS returning author: homepage keeps the session, sign-up link opens the workspace, sign out from the nav');
+ // AI analysis runs once per 24 hours: the server's refusal becomes a time the author can read,
+ // and the browser does not spend another request until then.
+ {
+ await page.goto(base+'/app.html');
+ await page.locator('#lib-loading.hidden').waitFor({state:'attached'});
+ const nextAt=Date.now()+2*3600000;let cooldownHits=0;
+ await context.route('**/api/claude',route=>{cooldownHits++;route.fulfill({status:429,json:{error:{code:'ANALYSIS_COOLDOWN',message:'AI analysis runs once every 24 hours.',nextAt}}});});
+ const first=await page.evaluate(()=>AIEngine._callClaude(null,'system','user','Some manuscript text for the cooldown check.','deepCritique').then(()=>'ok').catch(e=>e.message));
+ assert.match(first,/once every 24 hours\. The next run is available/);
+ assert.equal(cooldownHits,1);
+ const second=await page.evaluate(()=>AIEngine._callClaude(null,'system','user','Different manuscript text for the cooldown check.','deepCritique').then(()=>'ok').catch(e=>e.message));
+ assert.match(second,/once every 24 hours/);
+ assert.equal(cooldownHits,1,'the browser remembers the next run time instead of asking again');
+ await context.unroute('**/api/claude');
+ await page.evaluate(()=>localStorage.removeItem('ml_ai_next_run'));
+ console.log('PASS AI analysis cooldown: refusal carries the next run time, no repeat request');
+ }
// Workspace demo: a visitor opens the real workspace on a public-domain sample with no
// account, no cloud, no API call, and nothing left in browser storage.
let apiCalls=0;page.on('request',req=>{if(req.url().includes('/api/'))apiCalls++;});
@@ -409,6 +426,26 @@ function pdf(){
await page.locator('[data-wsnav="characters"]').click();
await page.locator('#workspace-nav-content').getByText('Mary',{exact:false}).first().waitFor();
console.log('PASS workspace demo: real workspace on public-domain samples, no account, no API, nothing saved');
+ // Features page: no emoji, the product frame is real output, the copy names real features,
+ // and the surfaces move on hover.
+ await page.setViewportSize({width:1440,height:1000});
+ await page.goto(base+'/features.html');
+ await page.getByRole('heading',{name:/manuscript diagnostic/}).waitFor();
+ const fxText=await page.locator('main').innerText();
+ assert.ok(!/[\u{1F300}-\u{1FAFF}\u{2600}-\u{27BF}]/u.test(fxText),'no emoji on the features page');
+ assert.match(fxText,/described, not scored/);
+ assert.match(fxText,/Narrative structure/i);
+ assert.ok(!/Midpoint|Climax|Escalation/.test(fxText),'no act labels the engine does not measure');
+ assert.equal(await page.locator('.fx-curve').count(),1);
+ assert.equal(await page.locator('.fx-chips li').count(),20,'the genre count matches the app');
+ const card=page.locator('.fx-card').first();
+ await card.scrollIntoViewIfNeeded();
+ await page.waitForFunction(()=>document.querySelector('.fx-card').classList.contains('is-in'));
+ const box=await card.boundingBox();await page.mouse.move(box.x+box.width/2,box.y+box.height/2);
+ await page.waitForFunction(()=>{const el=document.querySelector('.fx-card');return el.matches(':hover')&&getComputedStyle(el).transform!=='none';},null,{timeout:5000});
+ assert.ok(true,'a card lifts on hover');
+ await page.screenshot({path:path.join(root,'test-results/features-desktop.png'),fullPage:true});
+ console.log('PASS features page: no emoji, real product frame, real feature list, hover motion');
await page.setViewportSize({width:375,height:812});
for(const filename of ['features.html','pricing.html','faq.html','blog.html','legal.html','profile.html']){
await page.goto(base+'/'+filename);