Upload 64 files
Browse files- app/__pycache__/config.cpython-311.pyc +0 -0
- app/config.py +7 -3
- frontend/dist/assets/index.css +210 -0
- frontend/src/App.tsx +194 -22
- frontend/src/index.css +210 -0
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 |
-
|
| 34 |
-
|
| 35 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 ${
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
| 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
|
|
|
|
| 382 |
|
| 383 |
if (!ready) {
|
| 384 |
return (
|
|
@@ -443,8 +585,8 @@ export default function App() {
|
|
| 443 |
</div>
|
| 444 |
|
| 445 |
{idleSignedOut ? (
|
| 446 |
-
<
|
| 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 |
-
</
|
| 459 |
) : null}
|
| 460 |
|
| 461 |
{isGuest && !idleSignedOut ? (
|
| 462 |
-
<
|
| 463 |
-
|
| 464 |
-
|
| 465 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 (
|
| 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 || "
|
| 591 |
</div>
|
| 592 |
<div className="counts">
|
| 593 |
-
<span className={
|
| 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 |
-
|
| 607 |
-
|
| 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 & 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 & 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 & professionals</h3>
|
| 298 |
+
<p>Turn stiff AI emails and reports into confident, natural communication.</p>
|
| 299 |
+
</article>
|
| 300 |
+
<article>
|
| 301 |
+
<h3>Bloggers & 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 & 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 & 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 |
}
|