idnameraj commited on
Commit
48d657f
·
verified ·
1 Parent(s): 430efad

Upload 64 files

Browse files
app/__pycache__/config.cpython-311.pyc CHANGED
Binary files a/app/__pycache__/config.cpython-311.pyc and b/app/__pycache__/config.cpython-311.pyc differ
 
app/config.py CHANGED
@@ -30,9 +30,13 @@ SUPABASE_JWT_SECRET = os.environ.get("SUPABASE_JWT_SECRET") or ""
30
  AUTH_ENABLED = bool(SUPABASE_URL and SUPABASE_ANON_KEY and SUPABASE_SERVICE_ROLE_KEY)
31
 
32
  # Guest / homepage teaser (when auth is enabled and user is not signed in)
33
- GUEST_DAILY_REWRITES = int(os.environ.get("GUEST_DAILY_REWRITES", "1") or "1")
34
- GUEST_MAX_WORDS = int(os.environ.get("GUEST_MAX_WORDS", "100") or "100")
35
- GUEST_DAILY_WORD_CAP = int(os.environ.get("GUEST_DAILY_WORD_CAP", "100") or "100")
 
 
 
 
36
 
37
  # Hard logout after this many minutes with no mouse/keyboard/touch activity (0 = off)
38
  SESSION_IDLE_MINUTES = int(os.environ.get("SESSION_IDLE_MINUTES", "30") or "30")
 
30
  AUTH_ENABLED = bool(SUPABASE_URL and SUPABASE_ANON_KEY and SUPABASE_SERVICE_ROLE_KEY)
31
 
32
  # Guest / homepage teaser (when auth is enabled and user is not signed in)
33
+ # Hard-capped so a mis-set Space env cannot destroy the Free→Pro funnel
34
+ GUEST_DAILY_REWRITES = max(1, min(int(os.environ.get("GUEST_DAILY_REWRITES", "1") or "1"), 3))
35
+ GUEST_MAX_WORDS = max(20, min(int(os.environ.get("GUEST_MAX_WORDS", "100") or "100"), 150))
36
+ GUEST_DAILY_WORD_CAP = max(
37
+ GUEST_MAX_WORDS,
38
+ min(int(os.environ.get("GUEST_DAILY_WORD_CAP", str(GUEST_MAX_WORDS)) or str(GUEST_MAX_WORDS)), 300),
39
+ )
40
 
41
  # Hard logout after this many minutes with no mouse/keyboard/touch activity (0 = off)
42
  SESSION_IDLE_MINUTES = int(os.environ.get("SESSION_IDLE_MINUTES", "30") or "30")
frontend/dist/assets/index.css CHANGED
@@ -675,6 +675,206 @@ textarea {
675
  font-size: 0.78rem;
676
  }
677
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
678
  @media (max-width: 920px) {
679
  .topbar {
680
  flex-direction: column;
@@ -718,4 +918,14 @@ textarea {
718
  .pane textarea {
719
  min-height: 280px;
720
  }
 
 
 
 
 
 
 
 
 
 
721
  }
 
675
  font-size: 0.78rem;
676
  }
677
 
678
+ .teaser-cta {
679
+ display: flex;
680
+ flex-wrap: wrap;
681
+ align-items: center;
682
+ justify-content: space-between;
683
+ gap: 0.75rem 1rem;
684
+ }
685
+
686
+ .tool-hint {
687
+ font-size: 0.72rem;
688
+ color: var(--muted);
689
+ letter-spacing: 0.02em;
690
+ text-transform: none;
691
+ font-weight: 500;
692
+ }
693
+
694
+ .word-meter {
695
+ height: 3px;
696
+ background: rgba(18, 28, 26, 0.06);
697
+ }
698
+
699
+ .word-meter-fill {
700
+ height: 100%;
701
+ background: var(--accent);
702
+ transition: width 0.25s ease, background 0.2s ease;
703
+ }
704
+
705
+ .word-meter-fill.hot {
706
+ background: #b8860b;
707
+ }
708
+
709
+ .word-meter-fill.over {
710
+ background: var(--warn);
711
+ }
712
+
713
+ .landing {
714
+ margin-top: 2.75rem;
715
+ display: flex;
716
+ flex-direction: column;
717
+ gap: 2.75rem;
718
+ padding-bottom: 2rem;
719
+ }
720
+
721
+ .land-block h2 {
722
+ margin: 0;
723
+ font-family: var(--font-display);
724
+ font-size: clamp(1.6rem, 3vw, 2.1rem);
725
+ letter-spacing: -0.03em;
726
+ }
727
+
728
+ .land-lead {
729
+ margin: 0.45rem 0 0;
730
+ color: var(--ink-soft);
731
+ max-width: 34rem;
732
+ line-height: 1.45;
733
+ }
734
+
735
+ .steps {
736
+ list-style: none;
737
+ margin: 1.35rem 0 0;
738
+ padding: 0;
739
+ display: grid;
740
+ gap: 0.85rem;
741
+ }
742
+
743
+ .steps li {
744
+ display: flex;
745
+ gap: 0.9rem;
746
+ align-items: flex-start;
747
+ padding: 1rem 1.1rem;
748
+ border-radius: 14px;
749
+ border: 1px solid var(--panel-edge);
750
+ background: rgba(251, 252, 251, 0.72);
751
+ }
752
+
753
+ .step-num {
754
+ flex: 0 0 auto;
755
+ width: 1.85rem;
756
+ height: 1.85rem;
757
+ display: grid;
758
+ place-items: center;
759
+ border-radius: 999px;
760
+ background: var(--accent);
761
+ color: #f5faf8;
762
+ font-size: 0.85rem;
763
+ font-weight: 700;
764
+ }
765
+
766
+ .steps strong {
767
+ display: block;
768
+ margin-bottom: 0.2rem;
769
+ }
770
+
771
+ .steps p {
772
+ margin: 0;
773
+ color: var(--ink-soft);
774
+ font-size: 0.92rem;
775
+ line-height: 1.4;
776
+ }
777
+
778
+ .audience-grid {
779
+ margin-top: 1.25rem;
780
+ display: grid;
781
+ grid-template-columns: repeat(2, minmax(0, 1fr));
782
+ gap: 0.85rem;
783
+ }
784
+
785
+ .audience-grid article {
786
+ padding: 1.1rem 1.15rem;
787
+ border-radius: 14px;
788
+ border: 1px solid var(--panel-edge);
789
+ background: rgba(251, 252, 251, 0.72);
790
+ }
791
+
792
+ .audience-grid h3 {
793
+ margin: 0;
794
+ font-size: 1.02rem;
795
+ font-family: var(--font-display);
796
+ letter-spacing: -0.02em;
797
+ }
798
+
799
+ .audience-grid p {
800
+ margin: 0.4rem 0 0;
801
+ color: var(--ink-soft);
802
+ font-size: 0.9rem;
803
+ line-height: 1.45;
804
+ }
805
+
806
+ .pricing-grid {
807
+ margin-top: 1.25rem;
808
+ display: grid;
809
+ grid-template-columns: repeat(3, minmax(0, 1fr));
810
+ gap: 0.85rem;
811
+ }
812
+
813
+ .price-card {
814
+ padding: 1.2rem 1.15rem;
815
+ border-radius: 16px;
816
+ border: 1px solid var(--panel-edge);
817
+ background: rgba(251, 252, 251, 0.8);
818
+ display: flex;
819
+ flex-direction: column;
820
+ gap: 0.55rem;
821
+ }
822
+
823
+ .price-card-pro {
824
+ border-color: rgba(26, 92, 74, 0.35);
825
+ background: var(--accent-soft);
826
+ }
827
+
828
+ .price-card h3 {
829
+ margin: 0;
830
+ font-size: 1.05rem;
831
+ }
832
+
833
+ .price-amount {
834
+ margin: 0;
835
+ font-family: var(--font-display);
836
+ font-size: 1.85rem;
837
+ letter-spacing: -0.03em;
838
+ color: var(--ink);
839
+ }
840
+
841
+ .price-amount span {
842
+ font-size: 0.95rem;
843
+ color: var(--muted);
844
+ font-family: var(--font-body);
845
+ }
846
+
847
+ .price-card ul {
848
+ margin: 0;
849
+ padding-left: 1.1rem;
850
+ color: var(--ink-soft);
851
+ font-size: 0.88rem;
852
+ line-height: 1.55;
853
+ flex: 1;
854
+ }
855
+
856
+ .price-soon {
857
+ margin: 0;
858
+ font-size: 0.78rem;
859
+ color: var(--muted);
860
+ }
861
+
862
+ .site-footer {
863
+ margin-top: 0.5rem;
864
+ padding-top: 1.5rem;
865
+ border-top: 1px solid var(--panel-edge);
866
+ color: var(--muted);
867
+ font-size: 0.82rem;
868
+ line-height: 1.45;
869
+ }
870
+
871
+ .footer-brand {
872
+ margin: 0.55rem 0 0;
873
+ font-family: var(--font-display);
874
+ color: var(--ink-soft);
875
+ font-size: 1rem;
876
+ }
877
+
878
  @media (max-width: 920px) {
879
  .topbar {
880
  flex-direction: column;
 
918
  .pane textarea {
919
  min-height: 280px;
920
  }
921
+
922
+ .audience-grid,
923
+ .pricing-grid {
924
+ grid-template-columns: 1fr;
925
+ }
926
+
927
+ .teaser-cta {
928
+ flex-direction: column;
929
+ align-items: flex-start;
930
+ }
931
  }
frontend/src/App.tsx CHANGED
@@ -14,6 +14,16 @@ import type { PlanCard } from "./supabase";
14
 
15
  const MAX_CHARS = 50000;
16
 
 
 
 
 
 
 
 
 
 
 
17
  function wordCount(text: string): number {
18
  return text.trim() ? text.trim().split(/\s+/).length : 0;
19
  }
@@ -153,11 +163,13 @@ function AuthModal({
153
  function UpgradeCard({
154
  account,
155
  plans,
 
156
  onSignUp,
157
  onSignIn,
158
  }: {
159
  account: AccountInfo | null;
160
  plans: PlanCard[];
 
161
  onSignUp: () => void;
162
  onSignIn: () => void;
163
  }) {
@@ -166,7 +178,9 @@ function UpgradeCard({
166
 
167
  const free = plans.find((p) => p.id === "free");
168
  const pro = plans.find((p) => p.id === "pro");
169
- const isGuest = planId === "guest" || account?.role === "guest";
 
 
170
 
171
  return (
172
  <section className="upgrade-card" aria-label="Upgrade plans">
@@ -178,7 +192,7 @@ function UpgradeCard({
178
  </h3>
179
  <p>
180
  {isGuest
181
- ? `Preview is capped at ${account?.plan.max_words_per_request ?? 100} words and ${account?.plan.daily_rewrites ?? 1} rewrite/day. Sign up free for higher limits — or Pro for serious daily use.`
182
  : `You're on ${account?.plan.name ?? "Free"}. Pro gives up to ${pro?.max_words_per_request?.toLocaleString() ?? "2,000"} words per rewrite and ${pro?.daily_rewrites ?? 50} rewrites/day.`}
183
  </p>
184
  </div>
@@ -228,6 +242,120 @@ function UpgradeCard({
228
  );
229
  }
230
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
231
  export default function App() {
232
  const {
233
  ready,
@@ -259,7 +387,10 @@ export default function App() {
259
  const [authTitle, setAuthTitle] = useState<string | undefined>();
260
 
261
  const isGuest = Boolean(authEnabled && !session);
262
- const maxWords = account?.plan.max_words_per_request ?? (isGuest ? guestMaxWords : 50000);
 
 
 
263
 
264
  useEffect(() => {
265
  if (!copied) return;
@@ -274,6 +405,10 @@ export default function App() {
274
  }
275
  }, [session, refreshAccount]);
276
 
 
 
 
 
277
  useEffect(() => {
278
  const onKey = (e: KeyboardEvent) => {
279
  if ((e.metaKey || e.ctrlKey) && e.key === "Enter") {
@@ -292,10 +427,16 @@ export default function App() {
292
  setAuthOpen(true);
293
  }
294
 
 
 
 
 
 
 
295
  async function onRewrite() {
296
  const text = input.trim();
297
  if (!text) {
298
- setError("Paste some text first.");
299
  return;
300
  }
301
  if (text.length > MAX_CHARS) {
@@ -378,7 +519,8 @@ export default function App() {
378
 
379
  const inWords = wordCount(input);
380
  const outWords = wordCount(output);
381
- const overGuestCap = isGuest && inWords > maxWords;
 
382
 
383
  if (!ready) {
384
  return (
@@ -443,8 +585,8 @@ export default function App() {
443
  </div>
444
 
445
  {idleSignedOut ? (
446
- <p className="teaser-banner idle-banner" role="status">
447
- Signed out after {sessionIdleMinutes} minutes of inactivity.{" "}
448
  <button
449
  type="button"
450
  className="ghost-btn"
@@ -455,14 +597,23 @@ export default function App() {
455
  >
456
  Sign in again
457
  </button>
458
- </p>
459
  ) : null}
460
 
461
  {isGuest && !idleSignedOut ? (
462
- <p className="teaser-banner">
463
- Try a short rewrite free — up to <strong>{maxWords} words</strong>,{" "}
464
- {account?.plan.daily_rewrites ?? 1} per day. Sign up for more length and daily rewrites.
465
- </p>
 
 
 
 
 
 
 
 
 
466
  ) : null}
467
 
468
  <div className="stage">
@@ -474,6 +625,7 @@ export default function App() {
474
  <button
475
  key={t}
476
  type="button"
 
477
  className={tone === t ? "active" : ""}
478
  onClick={() => setTone(t)}
479
  >
@@ -481,6 +633,7 @@ export default function App() {
481
  </button>
482
  ))}
483
  </div>
 
484
  </div>
485
 
486
  <div className="tool-group">
@@ -509,6 +662,14 @@ export default function App() {
509
  </label>
510
 
511
  <div className="toolbar-actions">
 
 
 
 
 
 
 
 
512
  <button
513
  type="button"
514
  className="btn btn-quiet"
@@ -544,8 +705,8 @@ export default function App() {
544
  onChange={(e) => setInput(e.target.value)}
545
  placeholder={
546
  isGuest
547
- ? `Paste a short AI draft (up to ${maxWords} words)…`
548
- : "Paste your draft here…"
549
  }
550
  spellCheck
551
  />
@@ -585,12 +746,21 @@ export default function App() {
585
  </div>
586
  </section>
587
 
 
 
 
 
 
 
 
 
 
588
  <div className="statusbar">
589
  <div className={error ? "error" : loading ? "loading" : undefined}>
590
- {error || meta || "Ready to rewrite"}
591
  </div>
592
  <div className="counts">
593
- <span className={overGuestCap ? "over-limit" : undefined}>
594
  {inWords}
595
  {authEnabled ? ` / ${maxWords}` : ""} words in
596
  </span>
@@ -603,18 +773,20 @@ export default function App() {
603
  <UpgradeCard
604
  account={account}
605
  plans={plans}
606
- onSignUp={() =>
607
- openAuth(
608
- session ? "signup" : "signup",
609
- session ? "Upgrade to Pro" : "Create free account",
610
- )
611
- }
612
  onSignIn={() => openAuth("signin", "Sign in")}
613
  />
614
  ) : null}
615
 
616
  <p className="hint">Review the rewrite before you share or publish it.</p>
617
 
 
 
 
 
 
 
618
  {authEnabled ? (
619
  <AuthModal
620
  open={authOpen}
 
14
 
15
  const MAX_CHARS = 50000;
16
 
17
+ const SAMPLE_TEXT =
18
+ "Artificial intelligence has significantly transformed the way individuals create written content in recent years. Many professionals now rely on advanced language models to generate initial drafts quickly and efficiently. However, the resulting text can sometimes appear repetitive, overly formal, or lacking a natural human voice. Therefore, it is essential to carefully refine AI-generated material so that it communicates clearly, remains original in wording, and feels authentic to the intended audience.";
19
+
20
+ const TONE_HINT: Record<Tone, string> = {
21
+ Neutral: "Clear and balanced",
22
+ Casual: "Conversational",
23
+ Formal: "Professional",
24
+ Academic: "Scholarly",
25
+ };
26
+
27
  function wordCount(text: string): number {
28
  return text.trim() ? text.trim().split(/\s+/).length : 0;
29
  }
 
163
  function UpgradeCard({
164
  account,
165
  plans,
166
+ guestMaxWords,
167
  onSignUp,
168
  onSignIn,
169
  }: {
170
  account: AccountInfo | null;
171
  plans: PlanCard[];
172
+ guestMaxWords: number;
173
  onSignUp: () => void;
174
  onSignIn: () => void;
175
  }) {
 
178
 
179
  const free = plans.find((p) => p.id === "free");
180
  const pro = plans.find((p) => p.id === "pro");
181
+ const isGuest = planId === "guest" || account?.role === "guest" || !account?.email;
182
+ const previewWords = isGuest ? guestMaxWords : account?.plan.max_words_per_request ?? 100;
183
+ const previewRewrites = account?.plan.daily_rewrites ?? 1;
184
 
185
  return (
186
  <section className="upgrade-card" aria-label="Upgrade plans">
 
192
  </h3>
193
  <p>
194
  {isGuest
195
+ ? `Preview is capped at ${previewWords} words and ${previewRewrites} rewrite/day. Sign up free for higher limits — or Pro for serious daily use.`
196
  : `You're on ${account?.plan.name ?? "Free"}. Pro gives up to ${pro?.max_words_per_request?.toLocaleString() ?? "2,000"} words per rewrite and ${pro?.daily_rewrites ?? 50} rewrites/day.`}
197
  </p>
198
  </div>
 
242
  );
243
  }
244
 
245
+ function LandingSections({
246
+ plans,
247
+ onSignUp,
248
+ authEnabled,
249
+ }: {
250
+ plans: PlanCard[];
251
+ onSignUp: () => void;
252
+ authEnabled: boolean;
253
+ }) {
254
+ const free = plans.find((p) => p.id === "free");
255
+ const pro = plans.find((p) => p.id === "pro");
256
+ const guest = plans.find((p) => p.id === "guest");
257
+
258
+ return (
259
+ <div className="landing">
260
+ <section className="land-block">
261
+ <h2>How to use PlainRewrite</h2>
262
+ <p className="land-lead">Three steps from AI draft to natural wording.</p>
263
+ <ol className="steps">
264
+ <li>
265
+ <span className="step-num">1</span>
266
+ <div>
267
+ <strong>Paste your AI text</strong>
268
+ <p>Drop in a ChatGPT, Gemini, or Claude draft on the left.</p>
269
+ </div>
270
+ </li>
271
+ <li>
272
+ <span className="step-num">2</span>
273
+ <div>
274
+ <strong>Pick tone &amp; strength</strong>
275
+ <p>Neutral, Casual, Formal, or Academic — match the voice you need.</p>
276
+ </div>
277
+ </li>
278
+ <li>
279
+ <span className="step-num">3</span>
280
+ <div>
281
+ <strong>Rewrite, then unlock more</strong>
282
+ <p>Try Preview free, sign up for daily limits, go Pro when you write every day.</p>
283
+ </div>
284
+ </li>
285
+ </ol>
286
+ </section>
287
+
288
+ <section className="land-block">
289
+ <h2>Who it’s for</h2>
290
+ <p className="land-lead">Built for people who draft with AI and publish as themselves.</p>
291
+ <div className="audience-grid">
292
+ <article>
293
+ <h3>Students &amp; academic writers</h3>
294
+ <p>Refine AI-assisted notes into clearer Academic or Formal wording. Follow your institution’s rules.</p>
295
+ </article>
296
+ <article>
297
+ <h3>Freelancers &amp; professionals</h3>
298
+ <p>Turn stiff AI emails and reports into confident, natural communication.</p>
299
+ </article>
300
+ <article>
301
+ <h3>Bloggers &amp; SEO writers</h3>
302
+ <p>Refresh repetitive AI drafts into readable posts that still keep your meaning.</p>
303
+ </article>
304
+ <article>
305
+ <h3>Social &amp; content teams</h3>
306
+ <p>Humanize captions and scripts so they sound like your brand, not a model.</p>
307
+ </article>
308
+ </div>
309
+ </section>
310
+
311
+ <section className="land-block" id="plans">
312
+ <h2>Simple plans</h2>
313
+ <p className="land-lead">Start free. Upgrade when the preview isn’t enough.</p>
314
+ <div className="pricing-grid">
315
+ <article className="price-card">
316
+ <h3>Preview</h3>
317
+ <p className="price-amount">₹0</p>
318
+ <ul>
319
+ <li>{guest?.max_words_per_request ?? 100} words / rewrite</li>
320
+ <li>{guest?.daily_rewrites ?? 1} rewrite / day</li>
321
+ <li>No account needed</li>
322
+ </ul>
323
+ </article>
324
+ <article className="price-card">
325
+ <h3>Free</h3>
326
+ <p className="price-amount">₹0</p>
327
+ <ul>
328
+ <li>{free?.max_words_per_request ?? 400} words / rewrite</li>
329
+ <li>{free?.daily_rewrites ?? 5} rewrites / day</li>
330
+ <li>Google or email signup</li>
331
+ </ul>
332
+ {authEnabled ? (
333
+ <button type="button" className="btn btn-quiet" onClick={onSignUp}>
334
+ Create free account
335
+ </button>
336
+ ) : null}
337
+ </article>
338
+ <article className="price-card price-card-pro">
339
+ <h3>Pro</h3>
340
+ <p className="price-amount">₹{pro?.price_inr_monthly ?? 199}<span>/mo</span></p>
341
+ <ul>
342
+ <li>{(pro?.max_words_per_request ?? 2000).toLocaleString()} words / rewrite</li>
343
+ <li>{pro?.daily_rewrites ?? 50} rewrites / day</li>
344
+ <li>Best for daily AI drafts</li>
345
+ </ul>
346
+ <p className="price-soon">Checkout coming soon</p>
347
+ </article>
348
+ </div>
349
+ </section>
350
+
351
+ <footer className="site-footer">
352
+ <p>Review every rewrite before you share or publish. Follow your school or workplace AI policy.</p>
353
+ <p className="footer-brand">PlainRewrite</p>
354
+ </footer>
355
+ </div>
356
+ );
357
+ }
358
+
359
  export default function App() {
360
  const {
361
  ready,
 
387
  const [authTitle, setAuthTitle] = useState<string | undefined>();
388
 
389
  const isGuest = Boolean(authEnabled && !session);
390
+ // Guests always use config teaser limit (ignore inflated account payloads / env mistakes)
391
+ const maxWords = isGuest
392
+ ? guestMaxWords
393
+ : account?.plan.max_words_per_request ?? 50000;
394
 
395
  useEffect(() => {
396
  if (!copied) return;
 
405
  }
406
  }, [session, refreshAccount]);
407
 
408
+ useEffect(() => {
409
+ if (isGuest) setShowUpgrade(true);
410
+ }, [isGuest]);
411
+
412
  useEffect(() => {
413
  const onKey = (e: KeyboardEvent) => {
414
  if ((e.metaKey || e.ctrlKey) && e.key === "Enter") {
 
427
  setAuthOpen(true);
428
  }
429
 
430
+ function loadSample() {
431
+ setInput(SAMPLE_TEXT);
432
+ setError("");
433
+ setMeta("Sample loaded — hit Rewrite to try it.");
434
+ }
435
+
436
  async function onRewrite() {
437
  const text = input.trim();
438
  if (!text) {
439
+ setError("Paste some text first — or try the sample.");
440
  return;
441
  }
442
  if (text.length > MAX_CHARS) {
 
519
 
520
  const inWords = wordCount(input);
521
  const outWords = wordCount(output);
522
+ const overCap = authEnabled && inWords > maxWords;
523
+ const meterPct = authEnabled ? Math.min(100, Math.round((inWords / Math.max(maxWords, 1)) * 100)) : 0;
524
 
525
  if (!ready) {
526
  return (
 
585
  </div>
586
 
587
  {idleSignedOut ? (
588
+ <div className="teaser-banner idle-banner" role="status">
589
+ <span>Signed out after {sessionIdleMinutes} minutes of inactivity.</span>
590
  <button
591
  type="button"
592
  className="ghost-btn"
 
597
  >
598
  Sign in again
599
  </button>
600
+ </div>
601
  ) : null}
602
 
603
  {isGuest && !idleSignedOut ? (
604
+ <div className="teaser-banner teaser-cta">
605
+ <div>
606
+ You’re on <strong>Free Preview</strong> up to <strong>{maxWords} words</strong>,{" "}
607
+ {account?.plan.daily_rewrites ?? 1} rewrite/day. Sign up free for 400 words &amp; 5/day.
608
+ </div>
609
+ <button
610
+ type="button"
611
+ className="btn btn-primary btn-compact"
612
+ onClick={() => openAuth("signup", "Create free account")}
613
+ >
614
+ Unlock Free plan
615
+ </button>
616
+ </div>
617
  ) : null}
618
 
619
  <div className="stage">
 
625
  <button
626
  key={t}
627
  type="button"
628
+ title={TONE_HINT[t]}
629
  className={tone === t ? "active" : ""}
630
  onClick={() => setTone(t)}
631
  >
 
633
  </button>
634
  ))}
635
  </div>
636
+ <span className="tool-hint">{TONE_HINT[tone]}</span>
637
  </div>
638
 
639
  <div className="tool-group">
 
662
  </label>
663
 
664
  <div className="toolbar-actions">
665
+ <button
666
+ type="button"
667
+ className="btn btn-quiet"
668
+ onClick={loadSample}
669
+ disabled={loading}
670
+ >
671
+ Try sample
672
+ </button>
673
  <button
674
  type="button"
675
  className="btn btn-quiet"
 
705
  onChange={(e) => setInput(e.target.value)}
706
  placeholder={
707
  isGuest
708
+ ? `Paste a short AI draft (max ${maxWords} words on Preview)…`
709
+ : "Paste your AI draft here…"
710
  }
711
  spellCheck
712
  />
 
746
  </div>
747
  </section>
748
 
749
+ {authEnabled ? (
750
+ <div className="word-meter" aria-hidden>
751
+ <div
752
+ className={`word-meter-fill${overCap ? " over" : meterPct > 80 ? " hot" : ""}`}
753
+ style={{ width: `${meterPct}%` }}
754
+ />
755
+ </div>
756
+ ) : null}
757
+
758
  <div className="statusbar">
759
  <div className={error ? "error" : loading ? "loading" : undefined}>
760
+ {error || meta || "Paste AI text → pick a tone → Rewrite"}
761
  </div>
762
  <div className="counts">
763
+ <span className={overCap ? "over-limit" : undefined}>
764
  {inWords}
765
  {authEnabled ? ` / ${maxWords}` : ""} words in
766
  </span>
 
773
  <UpgradeCard
774
  account={account}
775
  plans={plans}
776
+ guestMaxWords={guestMaxWords}
777
+ onSignUp={() => openAuth("signup", "Create free account")}
 
 
 
 
778
  onSignIn={() => openAuth("signin", "Sign in")}
779
  />
780
  ) : null}
781
 
782
  <p className="hint">Review the rewrite before you share or publish it.</p>
783
 
784
+ <LandingSections
785
+ plans={plans}
786
+ authEnabled={authEnabled}
787
+ onSignUp={() => openAuth("signup", "Create free account")}
788
+ />
789
+
790
  {authEnabled ? (
791
  <AuthModal
792
  open={authOpen}
frontend/src/index.css CHANGED
@@ -675,6 +675,206 @@ textarea {
675
  font-size: 0.78rem;
676
  }
677
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
678
  @media (max-width: 920px) {
679
  .topbar {
680
  flex-direction: column;
@@ -718,4 +918,14 @@ textarea {
718
  .pane textarea {
719
  min-height: 280px;
720
  }
 
 
 
 
 
 
 
 
 
 
721
  }
 
675
  font-size: 0.78rem;
676
  }
677
 
678
+ .teaser-cta {
679
+ display: flex;
680
+ flex-wrap: wrap;
681
+ align-items: center;
682
+ justify-content: space-between;
683
+ gap: 0.75rem 1rem;
684
+ }
685
+
686
+ .tool-hint {
687
+ font-size: 0.72rem;
688
+ color: var(--muted);
689
+ letter-spacing: 0.02em;
690
+ text-transform: none;
691
+ font-weight: 500;
692
+ }
693
+
694
+ .word-meter {
695
+ height: 3px;
696
+ background: rgba(18, 28, 26, 0.06);
697
+ }
698
+
699
+ .word-meter-fill {
700
+ height: 100%;
701
+ background: var(--accent);
702
+ transition: width 0.25s ease, background 0.2s ease;
703
+ }
704
+
705
+ .word-meter-fill.hot {
706
+ background: #b8860b;
707
+ }
708
+
709
+ .word-meter-fill.over {
710
+ background: var(--warn);
711
+ }
712
+
713
+ .landing {
714
+ margin-top: 2.75rem;
715
+ display: flex;
716
+ flex-direction: column;
717
+ gap: 2.75rem;
718
+ padding-bottom: 2rem;
719
+ }
720
+
721
+ .land-block h2 {
722
+ margin: 0;
723
+ font-family: var(--font-display);
724
+ font-size: clamp(1.6rem, 3vw, 2.1rem);
725
+ letter-spacing: -0.03em;
726
+ }
727
+
728
+ .land-lead {
729
+ margin: 0.45rem 0 0;
730
+ color: var(--ink-soft);
731
+ max-width: 34rem;
732
+ line-height: 1.45;
733
+ }
734
+
735
+ .steps {
736
+ list-style: none;
737
+ margin: 1.35rem 0 0;
738
+ padding: 0;
739
+ display: grid;
740
+ gap: 0.85rem;
741
+ }
742
+
743
+ .steps li {
744
+ display: flex;
745
+ gap: 0.9rem;
746
+ align-items: flex-start;
747
+ padding: 1rem 1.1rem;
748
+ border-radius: 14px;
749
+ border: 1px solid var(--panel-edge);
750
+ background: rgba(251, 252, 251, 0.72);
751
+ }
752
+
753
+ .step-num {
754
+ flex: 0 0 auto;
755
+ width: 1.85rem;
756
+ height: 1.85rem;
757
+ display: grid;
758
+ place-items: center;
759
+ border-radius: 999px;
760
+ background: var(--accent);
761
+ color: #f5faf8;
762
+ font-size: 0.85rem;
763
+ font-weight: 700;
764
+ }
765
+
766
+ .steps strong {
767
+ display: block;
768
+ margin-bottom: 0.2rem;
769
+ }
770
+
771
+ .steps p {
772
+ margin: 0;
773
+ color: var(--ink-soft);
774
+ font-size: 0.92rem;
775
+ line-height: 1.4;
776
+ }
777
+
778
+ .audience-grid {
779
+ margin-top: 1.25rem;
780
+ display: grid;
781
+ grid-template-columns: repeat(2, minmax(0, 1fr));
782
+ gap: 0.85rem;
783
+ }
784
+
785
+ .audience-grid article {
786
+ padding: 1.1rem 1.15rem;
787
+ border-radius: 14px;
788
+ border: 1px solid var(--panel-edge);
789
+ background: rgba(251, 252, 251, 0.72);
790
+ }
791
+
792
+ .audience-grid h3 {
793
+ margin: 0;
794
+ font-size: 1.02rem;
795
+ font-family: var(--font-display);
796
+ letter-spacing: -0.02em;
797
+ }
798
+
799
+ .audience-grid p {
800
+ margin: 0.4rem 0 0;
801
+ color: var(--ink-soft);
802
+ font-size: 0.9rem;
803
+ line-height: 1.45;
804
+ }
805
+
806
+ .pricing-grid {
807
+ margin-top: 1.25rem;
808
+ display: grid;
809
+ grid-template-columns: repeat(3, minmax(0, 1fr));
810
+ gap: 0.85rem;
811
+ }
812
+
813
+ .price-card {
814
+ padding: 1.2rem 1.15rem;
815
+ border-radius: 16px;
816
+ border: 1px solid var(--panel-edge);
817
+ background: rgba(251, 252, 251, 0.8);
818
+ display: flex;
819
+ flex-direction: column;
820
+ gap: 0.55rem;
821
+ }
822
+
823
+ .price-card-pro {
824
+ border-color: rgba(26, 92, 74, 0.35);
825
+ background: var(--accent-soft);
826
+ }
827
+
828
+ .price-card h3 {
829
+ margin: 0;
830
+ font-size: 1.05rem;
831
+ }
832
+
833
+ .price-amount {
834
+ margin: 0;
835
+ font-family: var(--font-display);
836
+ font-size: 1.85rem;
837
+ letter-spacing: -0.03em;
838
+ color: var(--ink);
839
+ }
840
+
841
+ .price-amount span {
842
+ font-size: 0.95rem;
843
+ color: var(--muted);
844
+ font-family: var(--font-body);
845
+ }
846
+
847
+ .price-card ul {
848
+ margin: 0;
849
+ padding-left: 1.1rem;
850
+ color: var(--ink-soft);
851
+ font-size: 0.88rem;
852
+ line-height: 1.55;
853
+ flex: 1;
854
+ }
855
+
856
+ .price-soon {
857
+ margin: 0;
858
+ font-size: 0.78rem;
859
+ color: var(--muted);
860
+ }
861
+
862
+ .site-footer {
863
+ margin-top: 0.5rem;
864
+ padding-top: 1.5rem;
865
+ border-top: 1px solid var(--panel-edge);
866
+ color: var(--muted);
867
+ font-size: 0.82rem;
868
+ line-height: 1.45;
869
+ }
870
+
871
+ .footer-brand {
872
+ margin: 0.55rem 0 0;
873
+ font-family: var(--font-display);
874
+ color: var(--ink-soft);
875
+ font-size: 1rem;
876
+ }
877
+
878
  @media (max-width: 920px) {
879
  .topbar {
880
  flex-direction: column;
 
918
  .pane textarea {
919
  min-height: 280px;
920
  }
921
+
922
+ .audience-grid,
923
+ .pricing-grid {
924
+ grid-template-columns: 1fr;
925
+ }
926
+
927
+ .teaser-cta {
928
+ flex-direction: column;
929
+ align-items: flex-start;
930
+ }
931
  }