diff --git a/assets/writer-desk-mobile.jpg b/assets/writer-desk-mobile.jpg new file mode 100644 index 0000000..3727b00 Binary files /dev/null and b/assets/writer-desk-mobile.jpg differ diff --git a/assets/writer-desk.jpg b/assets/writer-desk.jpg new file mode 100644 index 0000000..0ce6c61 Binary files /dev/null and b/assets/writer-desk.jpg differ diff --git a/features-init.js b/features-init.js index b77e36c..3e358f1 100644 --- a/features-init.js +++ b/features-init.js @@ -29,6 +29,14 @@ frame.addEventListener('pointerleave', () => { if (raf) cancelAnimationFrame(raf); raf = 0; win.style.transform = ''; }); } + // The photograph behind the hero drifts slower than the page. + const art = document.querySelector('.fx-hero-art img'); + if (art) { + let ticking = false; + const drift = () => { ticking = false; const y = Math.min(window.scrollY, 900); art.style.transform = 'translateY(' + (y * 0.18).toFixed(1) + 'px)'; }; + addEventListener('scroll', () => { if (!ticking) { ticking = true; requestAnimationFrame(drift); } }, { passive: true }); + } + // Each card's warm glow follows the pointer. document.querySelectorAll('.fx-card').forEach(card => { card.addEventListener('pointermove', e => { diff --git a/features.css b/features.css index 445af72..931791a 100644 --- a/features.css +++ b/features.css @@ -17,8 +17,9 @@ .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)} +/* Hero, over a photograph: full-bleed, washed toward the page colour on the copy side and + at the bottom edge, so the type stays legible in both themes. */ +.fx-hero{position:relative;isolation:isolate;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} @@ -132,3 +133,8 @@ .fx-card,.fx-chips li,.fx-window,.fx-icon,.fx-inspect{transition:none} .fx-card:hover,.fx-chips li:hover{transform:none} } + +.fx-hero-art{position:absolute;top:-1px;bottom:0;left:50%;width:100vw;transform:translateX(-50%);z-index:-1;overflow:hidden;pointer-events:none} +.fx-hero-art img{width:100%;height:112%;object-fit:cover;object-position:center 64%;opacity:.78;filter:saturate(.9);will-change:transform} +[data-theme=light] .fx-hero-art img{opacity:.5;filter:saturate(.75) brightness(1.1)} +.fx-hero-art::after{content:"";position:absolute;inset:0;background:linear-gradient(90deg,color-mix(in srgb,var(--bg) 78%,transparent) 0%,color-mix(in srgb,var(--bg) 40%,transparent) 45%,color-mix(in srgb,var(--bg) 8%,transparent) 100%),linear-gradient(180deg,color-mix(in srgb,var(--bg) 35%,transparent) 0%,transparent 25%,transparent 62%,var(--bg) 100%)} diff --git a/features.html b/features.html index 66833ee..35f1111 100644 --- a/features.html +++ b/features.html @@ -11,9 +11,9 @@ - + - + @@ -40,6 +40,7 @@
+
FEATURES

The manuscript diagnostic built for serious authors.

@@ -183,7 +184,7 @@

Your manuscript already contains the evidence.
See what it says.

- + diff --git a/pricing-init.js b/pricing-init.js index 6465696..76dcf03 100644 --- a/pricing-init.js +++ b/pricing-init.js @@ -1,17 +1,31 @@ -// Pricing page — Firebase init + checkout + FAQ -firebase.initializeApp(FIREBASE_CONFIG); -document.querySelectorAll('.faq-item').forEach(function(item){item.querySelector('.faq-q').addEventListener('click',function(){item.classList.toggle('open')})}); -document.querySelectorAll('.checkout-tier').forEach(function(btn){btn.addEventListener('click',async function(){ - // Wait for the restored session rather than reading currentUser before Firebase has loaded it. - var user=await new Promise(function(resolve){var stop=firebase.auth().onAuthStateChanged(function(u){stop();resolve(u)})}); - if(!user){alert('Please sign in first at the home page');window.location.href='index.html';return} - var plan=btn.dataset.plan;btn.textContent='Redirecting...';btn.disabled=true; - try{ - var hdrs={'content-type':'application/json'}; - try{hdrs['authorization']='Bearer '+await user.getIdToken()}catch(e){} - var resp=await fetch('/api/checkout',{method:'POST',headers:hdrs,body:JSON.stringify({userId:user.uid,email:user.email,plan})}); - var data=await resp.json(); - if(data.url)window.location.href=data.url; - else{alert(data.error||'Checkout failed');btn.textContent='Get '+plan.charAt(0).toUpperCase()+plan.slice(1);btn.disabled=false} - }catch(e){alert('Error: '+e.message);btn.textContent='Get '+plan.charAt(0).toUpperCase()+plan.slice(1);btn.disabled=false} -})}); +// Pricing page: FAQ and checkout. Sign-in and the header live in landing-init.js. The page must +// read and its FAQ must work even when the Firebase SDK cannot load; only checkout needs it. +(function(){ + 'use strict'; + var hasFirebase=typeof firebase!=='undefined'&&typeof FIREBASE_CONFIG!=='undefined'; + if(hasFirebase&&!firebase.apps.length)firebase.initializeApp(FIREBASE_CONFIG); + document.querySelectorAll('.faq-item').forEach(function(item){ + var q=item.querySelector('.faq-q'); + q.addEventListener('click',function(){var open=item.classList.toggle('open');q.setAttribute('aria-expanded',String(open));}); + }); + function openSignIn(){ + var signIn=document.getElementById('nav-signin'); + if(signIn)signIn.click();else window.location.href='index.html#signup'; + } + document.querySelectorAll('.checkout-tier').forEach(function(btn){btn.addEventListener('click',async function(){ + var label=btn.dataset.label||btn.textContent; + if(!hasFirebase){openSignIn();return;} + // Wait for the restored session rather than reading currentUser before Firebase has loaded it. + var user=await new Promise(function(resolve){var stop=firebase.auth().onAuthStateChanged(function(u){stop();resolve(u)})}); + if(!user){openSignIn();return;} + var plan=btn.dataset.plan;btn.textContent='Redirecting…';btn.disabled=true; + try{ + var hdrs={'content-type':'application/json'}; + try{hdrs['authorization']='Bearer '+await user.getIdToken()}catch(e){} + var resp=await fetch('/api/checkout',{method:'POST',headers:hdrs,body:JSON.stringify({userId:user.uid,email:user.email,plan})}); + var data=await resp.json(); + if(data.url)window.location.href=data.url; + else{alert(data.error||'Checkout failed');btn.textContent=label;btn.disabled=false} + }catch(e){alert('Error: '+e.message);btn.textContent=label;btn.disabled=false} + })}); +})(); diff --git a/pricing.css b/pricing.css new file mode 100644 index 0000000..4e5943c --- /dev/null +++ b/pricing.css @@ -0,0 +1,48 @@ +/* Pricing page, on the features page's base (features.css). */ +.pr-hero{text-align:center;padding-top:clamp(1rem,4vw,3rem);padding-bottom:clamp(2rem,4vw,3rem)} +.pr-hero h1{margin:0 auto;max-width:16ch} +.pr-hero .fx-lede{margin:var(--space-6) auto var(--space-4);max-width:58ch} + +.pr-plans{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:var(--space-6);align-items:stretch;padding-bottom:clamp(3rem,7vw,6rem)} +.pr-plan{display:flex;flex-direction:column} +.pr-plan .fx-list{flex:1;margin-bottom:var(--space-8)} +.pr-plan .fx-list li{color:var(--muted)} +.pr-plan .fx-list li:first-child{color:var(--text)} +.pr-price{display:flex;align-items:baseline;gap:.5rem;margin:.2rem 0 var(--space-4)} +.pr-amount{font-family:var(--display);font-size:3rem;font-weight:600;line-height:1;color:var(--text)} +.pr-per{font-size:var(--text-sm);color:var(--muted)} +.pr-desc{font-size:var(--text-sm);min-height:3.2em} +.pr-cta{width:100%;justify-content:center;margin:0} +.pr-fine{font-size:var(--text-xs);color:var(--muted);margin-top:var(--space-3);text-align:center} +.pr-featured{border-color:color-mix(in srgb,var(--gold) 55%,var(--line));box-shadow:0 30px 70px -40px rgba(0,0,0,.7)} +.pr-flag{position:absolute;top:-.7rem;left:var(--space-8);font-size:var(--text-xs);letter-spacing:.12em;text-transform:uppercase;color:var(--bg);background:var(--gold);border-radius:999px;padding:.25rem .7rem} + +.pr-how{display:grid;grid-template-columns:minmax(0,4fr) minmax(0,8fr);gap:clamp(2rem,5vw,5rem);align-items:start;border-top:1px solid var(--line);border-bottom:1px solid var(--line);padding-top:clamp(3rem,6vw,5rem);padding-bottom:clamp(3rem,6vw,5rem)} +.pr-facts{margin:0;display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:var(--space-6)} +.pr-facts div{padding-left:var(--space-4);border-left:2px solid var(--gold);transition:border-color .4s var(--ease),transform .4s var(--ease)} +.pr-facts div:hover{transform:translateX(3px);border-color:var(--text)} +.pr-facts dt{font-family:var(--display);font-size:1.25rem;color:var(--text);margin-bottom:.35rem} +.pr-facts dd{margin:0;font-size:var(--text-sm);color:var(--muted);line-height:1.55} + +.pr-faq{padding-top:clamp(3rem,6vw,5rem)} +.pr-faq h2{margin-bottom:var(--space-8)} +.faq-list{max-width:72ch} +.faq-item{border-top:1px solid var(--line)} +.faq-item:last-child{border-bottom:1px solid var(--line)} +.faq-q{display:flex;justify-content:space-between;align-items:center;gap:var(--space-4);width:100%;text-align:left;background:none;border:0;padding:1rem 0;font:600 1.05rem/1.3 var(--display);color:var(--text);cursor:pointer;transition:color .3s var(--ease)} +.faq-q::after{content:"+";font-family:var(--body);font-size:1.1rem;color:var(--gold);transition:transform .35s var(--ease)} +.faq-q:hover{color:var(--gold)} +.faq-item.open .faq-q::after{transform:rotate(45deg)} +.faq-a{display:grid;grid-template-rows:0fr;transition:grid-template-rows .45s var(--ease)} +.faq-a>p{overflow:hidden;margin:0;font-size:var(--text-sm);line-height:1.6;padding-bottom:0;transition:padding .45s var(--ease)} +.faq-item.open .faq-a{grid-template-rows:1fr} +.faq-item.open .faq-a>p{padding-bottom:1rem} + +@media(max-width:960px){ + .pr-plans{grid-template-columns:1fr;max-width:520px;margin-inline:auto} + .pr-how{grid-template-columns:1fr} + .pr-facts{grid-template-columns:1fr} +} +@media(prefers-reduced-motion:reduce){ + .faq-a,.faq-q::after,.pr-facts div{transition:none} +} diff --git a/pricing.html b/pricing.html index 1b114d2..462ad99 100644 --- a/pricing.html +++ b/pricing.html @@ -1,166 +1,154 @@ - - - -AuthorScrolls - Pricing - - - - - +
+ Premium +
$15per month
+

For a writer on deadline: more room, and the deepest reading on the strongest model.

+ + +

Cancel any time from your profile.

+
+ -
-

Simple, Transparent Pricing

-

The diagnostic engine is free. Upgrade for deeper critique when you’re close to querying.

+
+
+ HOW THE ALLOWANCE WORKS +

Advice is advisory. The numbers stay yours.

+
+
+
One analysis run a day
The whole-manuscript reading (critique, roadmap, opening, breakdown, comparables, query letter, readers, market) runs together, once every 24 hours. Results stay open until the next run.
+
Requests for the rest
Ask your book, fixes and rewrites are counted per request against the daily allowance, 25 on Starter and 75 on Premium.
+
Scores never move
AI reading is marked as interpretation. It can explain a score or disagree with it; it cannot change one.
+
What leaves your browser
The diagnostic runs locally. AI features send the relevant passages to the provider when you choose to run them, and only then.
+
+
-
-
-
Free
-
$0/mo
-
The full structural diagnostic. No time limit, no manuscript limit.
-
    -
  • Full manuscript analysis engine
  • -
  • Plot, pacing, clarity, dialogue scoring
  • -
  • Copy editing issue detection
  • -
  • Show vs Tell detection
  • -
  • DNF risk prediction
  • -
  • Opening diagnosis
  • -
  • Blurb generator (5 styles)
  • -
  • Book preview (Kindle/iPad/Paperback)
  • -
  • Word export with highlights
  • -
  • AI deep critique
  • -
  • AI query letter
  • -
  • AI beta reader simulation
  • -
- -
- - +
+ QUESTIONS +

Before you decide.

+
+

No, and we won’t pretend it does. AuthorScrolls tells you where your manuscript is built unevenly, where the prose is getting in the way, and what a reader of your genre comes for. A line editor and a proofreader are still part of the path to publication. What it saves you is paying someone to find the problems you could have caught yourself.

+

The diagnostic engine runs in your browser. Your manuscripts are saved to your account, encrypted at rest. AI features send the relevant passages to the model provider only when you run them; on Starter that is OpenAI’s fast models, and on Premium the deepest features are read by Claude.

+

Requests go through our server with your account, never with a key in the browser. We send only the passages a feature needs, and we do not share your work with anyone else. The providers’ own data terms apply to what is sent; see our privacy policy for the current list.

+

A full analysis reads across your whole manuscript in several requests. One run a day keeps the allowance honest and, in our experience, matches how revision works: read the findings, revise, come back tomorrow.

+

Any time, from your profile. You drop to Free at the end of the billing period and keep your manuscripts, your versions and every measurement.

+

Word (.docx), PDF and plain text, up to 10 MB each.

+
+
-
-
Premium
-
$15/mo
-
For writers on deadline — querying agents or preparing to self-publish.
-
    -
  • Everything in Starter
  • -
  • 75 AI calls per day
  • -
  • Beta reader simulation (5 personas)
  • -
  • Market readiness score
  • -
  • Chapter-by-chapter breakdown
  • -
  • AI weakness diagnosis
  • -
  • Genre-specific guidance
  • -
  • Priority support
  • -
  • Unlimited cloud storage
  • -
- -
-
+
+

Start with the numbers.
Add the reading when you need it.

+ +

No credit card. Your manuscript stays private.

+
+ -
-

Frequently Asked Questions

-
-
Does this replace my editor?
-
No — and we won’t pretend it does. AuthorScrolls is a structural diagnostic: it tells you where your manuscript has problems with pacing, hook strength, plot arc, dialogue, and clarity. A copy editor, line editor, and proofreader are still part of the path to publication. What we save you is the cost of hiring a developmental editor to find problems you could have caught yourself — and the embarrassment of submitting to agents before your opening chapter is working.
-
-
-
What AI models do you use?
-
We use Claude by Anthropic for deep narrative analysis and GPT-4.1-mini by OpenAI for faster tasks like comparable titles and query letters. All processing happens server-side — your manuscript never touches the browser.
-
-
-
Is my manuscript safe?
-
Yes. Your manuscripts are stored in encrypted Firebase storage. AI providers (Anthropic/OpenAI) do not store or train on your text. We never share your work with anyone.
-
-
-
Can I cancel anytime?
-
Absolutely. Cancel from your profile page at any time. No contracts, no hidden fees. Your data is preserved for 30 days after cancellation.
-
-
-
What's the difference between the rules engine and AI?
-
The rules engine (free) runs locally in your browser — it detects patterns like passive voice, weak verbs, pacing issues, and show-vs-tell. AI features (paid) send excerpts to language models for deeper analysis: narrative critique, comparable titles, beta reader simulation, and more.
-
-
-
What file formats are supported?
-
.docx (Word), .pdf, and .txt files up to 10MB.
-
-
-
+ - + + + YOUR WRITING DESK AWAITS

Welcome back.

Sign in to return to your manuscripts.

+ +
or use your email
+
+ + +
+

+ +
+ +

New to AuthorScrolls?

+ +
- - - - + + + + diff --git a/scripts/browser-smoke.js b/scripts/browser-smoke.js index e0c6d9b..7407938 100644 --- a/scripts/browser-smoke.js +++ b/scripts/browser-smoke.js @@ -445,7 +445,20 @@ function pdf(){ 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'); + assert.ok(await page.locator('.fx-hero-art img').evaluate(img=>img.complete&&img.naturalWidth>0),'the photograph behind the hero loaded'); + console.log('PASS features page: no emoji, real product frame, real feature list, hover motion, photograph'); + // Pricing: three plans, two checkout buttons, honest allowance copy, FAQ that opens. + await page.goto(base+'/pricing.html'); + await page.getByRole('heading',{name:/The diagnostic is free/}).waitFor(); + const prText=await page.locator('main').innerText(); + assert.ok(!/[\u{1F300}-\u{1FAFF}\u{2600}-\u{27BF}]/u.test(prText),'no emoji on the pricing page'); + assert.equal(await page.locator('.pr-plan').count(),3); + assert.deepEqual(await page.locator('.checkout-tier').evaluateAll(b=>b.map(x=>x.dataset.plan)),['starter','premium']); + assert.match(prText,/One full AI analysis run every 24 hours/); + assert.match(prText,/25 AI requests a day/);assert.match(prText,/75 AI requests a day/); + await page.locator('.faq-q').first().click(); + assert.equal(await page.locator('.faq-item.open').count(),1,'the FAQ opens'); + console.log('PASS pricing page: no emoji, three plans, checkout wiring, allowance copy, FAQ'); 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); diff --git a/server.js b/server.js index 534eb68..6652836 100644 --- a/server.js +++ b/server.js @@ -2,7 +2,7 @@ 'use strict'; const http=require('node:http'),fs=require('node:fs'),path=require('node:path'); const root=__dirname; -const mime={'.txt':'text/plain; charset=utf-8','.html':'text/html','.css':'text/css','.js':'application/javascript','.mjs':'application/javascript','.webp':'image/webp','.png':'image/png','.svg':'image/svg+xml','.ico':'image/x-icon','.json':'application/json'}; +const mime={'.txt':'text/plain; charset=utf-8','.html':'text/html','.css':'text/css','.js':'application/javascript','.mjs':'application/javascript','.webp':'image/webp','.png':'image/png','.jpg':'image/jpeg','.jpeg':'image/jpeg','.svg':'image/svg+xml','.ico':'image/x-icon','.json':'application/json'}; const apiRoutes=new Set(['claude','checkout','webhook','grammar','redeem','notify','account']); const publicFiles=new Set(fs.readdirSync(root).filter(name=> /\.(html|css|js)$/.test(name)&&!name.endsWith('.test.js')&&!['server.js'].includes(name) @@ -31,7 +31,7 @@ const server=http.createServer(async(req,res)=>{ if(!['GET','HEAD'].includes(req.method)){res.status(405).end();return;} const relative=pathname==='/'?'index.html':pathname.slice(1); // Public assets: flat files, plus the workspace demo's public-domain samples. - const asset=/^assets\/[a-zA-Z0-9_.-]+\.(webp|png|svg|ico|js|mjs)$/.test(relative)||/^assets\/samples\/[a-zA-Z0-9_-]+\.txt$/.test(relative); + const asset=/^assets\/[a-zA-Z0-9_.-]+\.(webp|png|jpg|jpeg|svg|ico|js|mjs)$/.test(relative)||/^assets\/samples\/[a-zA-Z0-9_-]+\.txt$/.test(relative); if(!publicFiles.has(relative)&&!asset){res.status(404).end('Not found');return;} try{ const content=await fs.promises.readFile(path.join(root,relative));