Merge branch 'frontend-upgrade'

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Deeman
2026-02-19 20:49:07 +01:00
16 changed files with 1102 additions and 798 deletions

View File

@@ -1,39 +1,36 @@
{% extends "base.html" %} {% extends "base.html" %}
{% block title %}Sign In - {{ config.APP_NAME }}{% endblock %} {% block title %}Sign In {{ config.APP_NAME }}{% endblock %}
{% block content %} {% block content %}
<main class="container"> <main class="container-page">
<article style="max-width: 400px; margin: 4rem auto;"> <div class="auth-card">
<header> <h1>Sign in to BeanFlows</h1>
<h1>Sign In</h1> <p class="subtitle">Enter your email. We'll send a link &mdash; no password needed.</p>
<p>Enter your email to receive a sign-in link.</p>
</header>
<form method="post"> <form method="post">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"> <input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<label for="email"> <div class="mb-4">
Email <label for="email" class="form-label">Email</label>
<input <input
type="email" type="email"
id="email" id="email"
name="email" name="email"
placeholder="you@example.com" class="form-input"
placeholder="trader@example.com"
required required
autofocus autofocus
> >
</label> </div>
<button type="submit">Send Sign-In Link</button> <button type="submit" class="btn w-full">Send sign-in link</button>
</form> </form>
<footer style="text-align: center; margin-top: 1rem;"> <p class="text-center text-sm text-stone mt-6">
<small> Don't have an account?
Don't have an account? <a href="{{ url_for('auth.signup') }}">Sign up</a>
<a href="{{ url_for('auth.signup') }}">Sign up</a> </p>
</small> </div>
</footer>
</article>
</main> </main>
{% endblock %} {% endblock %}

View File

@@ -1,35 +1,35 @@
{% extends "base.html" %} {% extends "base.html" %}
{% block title %}Check Your Email - {{ config.APP_NAME }}{% endblock %} {% block title %}Check Your Inbox — {{ config.APP_NAME }}{% endblock %}
{% block content %} {% block content %}
<main class="container"> <main class="container-page">
<article style="max-width: 400px; margin: 4rem auto; text-align: center;"> <div class="auth-card text-center">
<header> <div class="text-4xl mb-4">&#9993;</div>
<h1>Check Your Email</h1> <h1>Check your inbox</h1>
</header> <p class="subtitle">We sent a sign-in link to:</p>
<p>We've sent a sign-in link to:</p> <p class="font-mono text-espresso bg-latte rounded-lg px-4 py-2 mb-4 text-sm">{{ email }}</p>
<p><strong>{{ email }}</strong></p>
<p class="text-sm text-stone mb-6">Click the link in the email to sign in. It expires in {{ config.MAGIC_LINK_EXPIRY_MINUTES }} minutes.</p>
<p>Click the link in the email to sign in. The link expires in {{ config.MAGIC_LINK_EXPIRY_MINUTES }} minutes.</p>
<hr> <hr>
<details> <details class="faq-item text-left">
<summary>Didn't receive the email?</summary> <summary>Didn't receive the email?</summary>
<ul style="text-align: left;"> <div class="pb-4">
<li>Check your spam folder</li> <ul class="list-disc pl-5 space-y-1 text-sm text-stone mb-4">
<li>Make sure the email address is correct</li> <li>Check your spam folder</li>
<li>Wait a minute and try again</li> <li>Make sure the email address is correct</li>
</ul> <li>Wait a minute and try again</li>
</ul>
<form method="post" action="{{ url_for('auth.resend') }}"> <form method="post" action="{{ url_for('auth.resend') }}">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"> <input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<input type="hidden" name="email" value="{{ email }}"> <input type="hidden" name="email" value="{{ email }}">
<button type="submit" class="secondary outline">Resend Link</button> <button type="submit" class="btn-outline btn-sm">Resend link</button>
</form> </form>
</div>
</details> </details>
</article> </div>
</main> </main>
{% endblock %} {% endblock %}

View File

@@ -1,44 +1,40 @@
{% extends "base.html" %} {% extends "base.html" %}
{% block title %}Sign Up - {{ config.APP_NAME }}{% endblock %} {% block title %}Sign Up {{ config.APP_NAME }}{% endblock %}
{% block content %} {% block content %}
<main class="container"> <main class="container-page">
<article style="max-width: 400px; margin: 4rem auto;"> <div class="auth-card">
<header> <h1>Create your BeanFlows account</h1>
<h1>Create Account</h1> <p class="subtitle">Free plan. Real data. No credit card.</p>
<p>Enter your email to get started.</p>
</header>
<form method="post"> <form method="post">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"> <input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<input type="hidden" name="plan" value="{{ plan }}"> <input type="hidden" name="plan" value="{{ plan }}">
<label for="email"> <div class="mb-4">
Email <label for="email" class="form-label">Email</label>
<input <input
type="email" type="email"
id="email" id="email"
name="email" name="email"
placeholder="you@example.com" class="form-input"
placeholder="trader@example.com"
required required
autofocus autofocus
> >
</label> {% if plan and plan != 'free' %}
<p class="form-hint mt-2">You'll be able to subscribe to the <strong>{{ plan | title }}</strong> plan after signing up.</p>
{% if plan and plan != 'free' %} {% endif %}
<small>You'll be able to subscribe to the <strong>{{ plan | title }}</strong> plan after signing up.</small> </div>
{% endif %}
<button type="submit" class="btn w-full">Create account</button>
<button type="submit">Create Account</button>
</form> </form>
<footer style="text-align: center; margin-top: 1rem;"> <p class="text-center text-sm text-stone mt-6">
<small> Already have an account?
Already have an account? <a href="{{ url_for('auth.login') }}">Sign in</a>
<a href="{{ url_for('auth.login') }}">Sign in</a> </p>
</small> </div>
</footer>
</article>
</main> </main>
{% endblock %} {% endblock %}

View File

@@ -1,119 +1,147 @@
{% extends "base.html" %} {% extends "base.html" %}
{% block title %}Pricing - {{ config.APP_NAME }}{% endblock %} {% block title %}Pricing {{ config.APP_NAME }}{% endblock %}
{% block content %} {% block content %}
<main class="container"> <main>
<header style="text-align: center; margin-bottom: 3rem;"> <!-- Hero -->
<h1>Simple, Transparent Pricing</h1> <section class="hero pb-12">
<p>Start free with coffee data. Upgrade when you need more.</p> <div class="container-page">
</header> <h1 class="heading-display">Pick your depth of data</h1>
<p>Every plan includes the full dashboard. Upgrade when you need more history, exports, or API access.</p>
</div>
</section>
<div class="grid"> <!-- Pricing Cards -->
<!-- Free Plan --> <section class="container-page pb-16">
<article> <div class="grid-3 items-start">
<header> <!-- Explorer (Free) -->
<h3>Free</h3> <div class="pricing-card">
<p><strong style="font-size: 2rem;">$0</strong> <small>/month</small></p> <div class="mb-6">
</header> <h3 class="text-xl mb-1">Explorer</h3>
<ul> <div class="flex items-baseline gap-1">
<li>Coffee dashboard</li> <span class="text-4xl font-bold text-espresso font-mono">$0</span>
<li>Last 5 years of data</li> <span class="text-stone text-sm">/forever</span>
<li>Global &amp; country charts</li> </div>
<li>Community support</li> </div>
</ul> <ul class="space-y-2 text-sm text-stone mb-8 flex-1">
<footer> <li>&#10003; Full coffee dashboard</li>
{% if user %} <li>&#10003; Last 5 years of data</li>
{% if (user.plan or 'free') == 'free' %} <li>&#10003; Global &amp; country charts</li>
<button class="secondary" disabled>Current Plan</button> <li>&#10003; Community support</li>
</ul>
<div>
{% if user %}
{% if (user.plan or 'free') == 'free' %}
<button class="btn-outline w-full" disabled>Current plan</button>
{% else %}
<button class="btn-outline w-full" disabled>Explorer</button>
{% endif %}
{% else %} {% else %}
<button class="secondary" disabled>Free</button> <a href="{{ url_for('auth.signup', plan='free') }}" class="btn-outline w-full text-center">Start free</a>
{% endif %} {% endif %}
{% else %} </div>
<a href="{{ url_for('auth.signup', plan='free') }}" role="button" class="secondary">Get Started</a> </div>
{% endif %}
</footer>
</article>
<!-- Starter Plan --> <!-- Trader (Starter) — highlighted -->
<article> <div class="pricing-card-highlighted">
<header> <span class="pricing-badge">Most popular</span>
<h3>Starter</h3> <div class="mb-6 pt-2">
<p><strong style="font-size: 2rem;">TBD</strong> <small>/month</small></p> <h3 class="text-xl mb-1">Trader</h3>
</header> <div class="flex items-baseline gap-1">
<ul> <span class="text-4xl font-bold text-espresso font-mono">TBD</span>
<li>Full coffee history (18+ years)</li> <span class="text-stone text-sm">/mo</span>
<li>CSV data export</li> </div>
<li>REST API access (10k calls/mo)</li> </div>
<li>Email support</li> <ul class="space-y-2 text-sm text-stone mb-8 flex-1">
</ul> <li>&#10003; Full coffee history (18+ years)</li>
<footer> <li>&#10003; CSV data export</li>
{% if user %} <li>&#10003; REST API access (10k calls/mo)</li>
{% if (user.plan or 'free') == 'starter' %} <li>&#10003; Email support</li>
<button class="secondary" disabled>Current Plan</button> </ul>
<div>
{% if user %}
{% if (user.plan or 'free') == 'starter' %}
<button class="btn w-full" disabled>Current plan</button>
{% else %}
<form method="post" action="{{ url_for('billing.checkout', plan='starter') }}">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<button type="submit" class="btn w-full">Upgrade</button>
</form>
{% endif %}
{% else %} {% else %}
<form method="post" action="{{ url_for('billing.checkout', plan='starter') }}"> <a href="{{ url_for('auth.signup', plan='starter') }}" class="btn w-full text-center">Get started</a>
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<button type="submit">Upgrade</button>
</form>
{% endif %} {% endif %}
{% else %} </div>
<a href="{{ url_for('auth.signup', plan='starter') }}" role="button">Get Started</a> </div>
{% endif %}
</footer>
</article>
<!-- Pro Plan --> <!-- Analyst (Pro) -->
<article> <div class="pricing-card">
<header> <div class="mb-6">
<h3>Pro</h3> <h3 class="text-xl mb-1">Analyst</h3>
<p><strong style="font-size: 2rem;">TBD</strong> <small>/month</small></p> <div class="flex items-baseline gap-1">
</header> <span class="text-4xl font-bold text-espresso font-mono">TBD</span>
<ul> <span class="text-stone text-sm">/mo</span>
<li>All 65 USDA commodities</li> </div>
<li>Unlimited API calls</li> </div>
<li>CSV &amp; API export</li> <ul class="space-y-2 text-sm text-stone mb-8 flex-1">
<li>Priority support</li> <li>&#10003; All 65 USDA commodities</li>
</ul> <li>&#10003; Unlimited API calls</li>
<footer> <li>&#10003; CSV &amp; API export</li>
{% if user %} <li>&#10003; Priority support</li>
{% if (user.plan or 'free') == 'pro' %} </ul>
<button class="secondary" disabled>Current Plan</button> <div>
{% if user %}
{% if (user.plan or 'free') == 'pro' %}
<button class="btn-outline w-full" disabled>Current plan</button>
{% else %}
<form method="post" action="{{ url_for('billing.checkout', plan='pro') }}">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<button type="submit" class="btn-outline w-full">Upgrade</button>
</form>
{% endif %}
{% else %} {% else %}
<form method="post" action="{{ url_for('billing.checkout', plan='pro') }}"> <a href="{{ url_for('auth.signup', plan='pro') }}" class="btn-outline w-full text-center">Get started</a>
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<button type="submit">Upgrade</button>
</form>
{% endif %} {% endif %}
{% else %} </div>
<a href="{{ url_for('auth.signup', plan='pro') }}" role="button">Get Started</a> </div>
{% endif %} </div>
</footer> </section>
</article>
</div>
<!-- FAQ --> <!-- FAQ -->
<section style="margin-top: 4rem; max-width: 600px; margin-left: auto; margin-right: auto;"> <section class="container-page pb-16 max-w-2xl mx-auto">
<h2>Frequently Asked Questions</h2> <div class="section-heading">
<h2>Frequently asked questions</h2>
</div>
<details> <details class="faq-item">
<summary>Where does the data come from?</summary> <summary>Where does the data come from?</summary>
<p>All data comes from the USDA Production, Supply &amp; Distribution (PSD) Online database, which is freely available. We process and transform it daily into analytics-ready metrics.</p> <p>All data comes from the USDA Production, Supply &amp; Distribution (PSD) Online database, which is freely available. We extract, transform, and structure it daily into analytics-ready metrics.</p>
</details> </details>
<details> <details class="faq-item">
<summary>What's the difference between 5 years and 18 years of history?</summary>
<p>The free Explorer plan shows the last 5 market years. Trader and Analyst plans unlock the full dataset going back to 2006, giving you deeper context for long-term trend analysis.</p>
</details>
<details class="faq-item">
<summary>Can I change plans later?</summary> <summary>Can I change plans later?</summary>
<p>Yes. Upgrade or downgrade at any time. Changes take effect immediately with prorated billing.</p> <p>Yes. Upgrade or downgrade at any time. Changes take effect immediately with prorated billing handled by Paddle.</p>
</details> </details>
<details> <details class="faq-item">
<summary>What commodities are available on Pro?</summary> <summary>What format is the CSV export?</summary>
<p>All 65 commodities tracked by USDA PSD, including coffee, cocoa, sugar, cotton, grains, oilseeds, and more.</p> <p>Standard CSV with headers. One row per country per market year. Compatible with Excel, Google Sheets, Python, R, and any tool that reads CSV.</p>
</details> </details>
<details> <details class="faq-item">
<summary>How do I cancel?</summary> <summary>How does the API work?</summary>
<p>Cancel anytime from your dashboard settings. You keep access until the end of your billing period.</p> <p>Generate an API key from your dashboard settings. Use Bearer token authentication with standard REST endpoints for commodity listing, time series, and country data. Full docs available after signup.</p>
</details>
<details class="faq-item">
<summary>Is the free plan really free forever?</summary>
<p>Yes. No trial period, no credit card required. The Explorer plan is free for as long as you use it.</p>
</details> </details>
</section> </section>
</main> </main>

View File

@@ -1,26 +1,27 @@
{% extends "base.html" %} {% extends "base.html" %}
{% block title %}Success! - {{ config.APP_NAME }}{% endblock %} {% block title %}You're All Set — {{ config.APP_NAME }}{% endblock %}
{% block content %} {% block content %}
<main class="container"> <main class="container-page">
<article style="max-width: 500px; margin: 4rem auto; text-align: center;"> <div class="auth-card text-center">
<header> <div class="mb-4">
<h1>🎉 Welcome Aboard!</h1> <svg class="mx-auto" width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="#15803D" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
</header> <circle cx="12" cy="12" r="10"/>
<path d="M9 12l2 2 4-4"/>
<p>Your subscription is now active. You have full access to all features included in your plan.</p> </svg>
</div>
<p> <h1>You're all set</h1>
<a href="{{ url_for('dashboard.index') }}" role="button">Go to Dashboard</a> <p class="subtitle">Your subscription is now active. You have full access to all features included in your plan.</p>
</p>
<a href="{{ url_for('dashboard.index') }}" class="btn w-full mb-4">Go to Dashboard</a>
<hr> <hr>
<p><small> <p class="text-sm text-stone mt-4">
Need to manage your subscription? Visit Need to manage your subscription? Visit
<a href="{{ url_for('dashboard.settings') }}">account settings</a>. <a href="{{ url_for('dashboard.settings') }}">account settings</a>.
</small></p> </p>
</article> </div>
</main> </main>
{% endblock %} {% endblock %}

View File

@@ -1,76 +1,98 @@
{% extends "base.html" %} {% extends "base.html" %}
{% block title %}Country Comparison - {{ config.APP_NAME }}{% endblock %} {% block title %}Country Comparison {{ config.APP_NAME }}{% endblock %}
{% block head %} {% block head %}
<script src="https://cdn.jsdelivr.net/npm/chart.js@4"></script> <script src="https://cdn.jsdelivr.net/npm/chart.js@4"></script>
{% endblock %} {% endblock %}
{% block content %} {% block content %}
<main class="container"> <main class="container-page py-8">
<header> <!-- Page Header -->
<div class="page-header">
<h1>Country Comparison</h1> <h1>Country Comparison</h1>
<p>Compare coffee metrics across producing and consuming countries.</p> <p>Compare coffee metrics across producing and consuming countries.</p>
</header> </div>
<!-- Filters --> <!-- Filters -->
<form id="country-form" method="get" action="{{ url_for('dashboard.countries') }}"> <div class="card mb-8">
<div class="grid"> <form id="country-form" method="get" action="{{ url_for('dashboard.countries') }}">
<label> <div class="grid-2">
Metric <div>
<select name="metric" onchange="this.form.submit()"> <label for="metric" class="form-label">Metric</label>
{% for m in ["Production", "Exports", "Imports", "Ending_Stocks"] %} <select name="metric" id="metric" class="form-input" onchange="this.form.submit()">
<option value="{{ m }}" {{ "selected" if metric == m }}>{{ m | replace("_", " ") }}</option> {% for m in ["Production", "Exports", "Imports", "Ending_Stocks"] %}
{% endfor %} <option value="{{ m }}" {{ "selected" if metric == m }}>{{ m | replace("_", " ") }}</option>
</select> {% endfor %}
</label> </select>
<label> </div>
Countries (select up to 10) <div>
<select name="country" multiple size="8" onchange="this.form.submit()"> <label for="country" class="form-label">Countries (select up to 10)</label>
{% for c in all_countries %} <select name="country" id="country" class="form-input" multiple size="8" onchange="this.form.submit()">
<option value="{{ c.country_code }}" {{ "selected" if c.country_code in selected_codes }}> {% for c in all_countries %}
{{ c.country_name }} <option value="{{ c.country_code }}" {{ "selected" if c.country_code in selected_codes }}>
</option> {{ c.country_name }}
{% endfor %} </option>
</select> {% endfor %}
</label> </select>
</div> </div>
</form> </div>
</form>
</div>
<!-- Chart --> <!-- Chart -->
{% if comparison_data %} {% if comparison_data %}
<section> <div class="chart-container mb-8">
<canvas id="comparisonChart" style="max-height: 500px;"></canvas> <canvas id="comparisonChart"></canvas>
</section> </div>
{% else %} {% else %}
<article style="text-align: center; color: var(--muted-color);"> <div class="plan-gate mb-8">
<p>Select countries above to see the comparison chart.</p> Select countries above to see the comparison chart.
</article> </div>
{% endif %} {% endif %}
<a href="{{ url_for('dashboard.index') }}" role="button" class="secondary outline">Back to Dashboard</a> <a href="{{ url_for('dashboard.index') }}" class="btn-outline">Back to Dashboard</a>
</main> </main>
{% endblock %} {% endblock %}
{% block scripts %} {% block scripts %}
<script> <script>
const COLORS = [ const CHART_COLORS = {
'#2563eb', '#dc2626', '#16a34a', '#ca8a04', '#9333ea', copper: '#B45309',
'#0891b2', '#e11d48', '#65a30d', '#d97706', '#7c3aed' roast: '#4A2C1A',
beanGreen: '#15803D',
forest: '#064E3B',
stone: '#78716C',
espresso: '#2C1810',
warning: '#D97706',
danger: '#EF4444',
parchment: '#E8DFD5',
};
const CHART_PALETTE = [
CHART_COLORS.copper,
CHART_COLORS.beanGreen,
CHART_COLORS.roast,
CHART_COLORS.forest,
CHART_COLORS.stone,
CHART_COLORS.warning,
CHART_COLORS.danger,
CHART_COLORS.espresso,
]; ];
Chart.defaults.font.family = "'DM Sans', sans-serif";
Chart.defaults.color = CHART_COLORS.stone;
Chart.defaults.borderColor = CHART_COLORS.parchment;
const rawData = {{ comparison_data | tojson }}; const rawData = {{ comparison_data | tojson }};
const metric = {{ metric | tojson }}; const metric = {{ metric | tojson }};
if (rawData.length > 0) { if (rawData.length > 0) {
// Group by country
const byCountry = {}; const byCountry = {};
for (const row of rawData) { for (const row of rawData) {
if (!byCountry[row.country_name]) byCountry[row.country_name] = []; if (!byCountry[row.country_name]) byCountry[row.country_name] = [];
byCountry[row.country_name].push(row); byCountry[row.country_name].push(row);
} }
// Collect all years
const allYears = [...new Set(rawData.map(r => r.market_year))].sort(); const allYears = [...new Set(rawData.map(r => r.market_year))].sort();
const datasets = Object.entries(byCountry).map(([name, rows], i) => { const datasets = Object.entries(byCountry).map(([name, rows], i) => {
@@ -78,7 +100,7 @@ if (rawData.length > 0) {
return { return {
label: name, label: name,
data: allYears.map(y => yearMap[y] ?? null), data: allYears.map(y => yearMap[y] ?? null),
borderColor: COLORS[i % COLORS.length], borderColor: CHART_PALETTE[i % CHART_PALETTE.length],
tension: 0.3, tension: 0.3,
spanGaps: true spanGaps: true
}; };

View File

@@ -1,100 +1,97 @@
{% extends "base.html" %} {% extends "base.html" %}
{% block title %}Dashboard - {{ config.APP_NAME }}{% endblock %} {% block title %}Dashboard {{ config.APP_NAME }}{% endblock %}
{% block head %} {% block head %}
<script src="https://cdn.jsdelivr.net/npm/chart.js@4"></script> <script src="https://cdn.jsdelivr.net/npm/chart.js@4"></script>
{% endblock %} {% endblock %}
{% block content %} {% block content %}
<main class="container"> <main class="container-page py-8">
<header> <!-- Page Header -->
<div class="page-header">
<h1>Coffee Dashboard</h1> <h1>Coffee Dashboard</h1>
<p>Welcome back{% if user.name %}, {{ user.name }}{% endif %}! Global coffee market data from USDA PSD.</p> <p>Welcome back{% if user.name %}, {{ user.name }}{% endif %}! Global coffee market data from USDA PSD.</p>
</header> </div>
<!-- Key Metric Cards --> <!-- Key Metric Cards -->
<div class="grid"> <div class="grid-4 mb-8">
<article> <div class="metric-card">
<header><small>Global Production (latest year)</small></header> <div class="metric-label">Global Production (latest year)</div>
<p style="font-size: 2rem; margin: 0;"> <div class="metric-value">{{ "{:,.0f}".format(latest.get("Production", 0)) }}</div>
<strong>{{ "{:,.0f}".format(latest.get("Production", 0)) }}</strong> <div class="metric-sub">1,000 60-kg bags</div>
</p> </div>
<small>1,000 60-kg bags</small>
</article>
<article> <div class="metric-card">
<header><small>Stock-to-Use Ratio</small></header> <div class="metric-label">Stock-to-Use Ratio</div>
<p style="font-size: 2rem; margin: 0;"> <div class="metric-value">
{% if stu_trend %} {% if stu_trend %}
<strong>{{ "{:.1f}".format(stu_trend[-1].get("Stock_to_Use_Ratio_pct", 0)) }}%</strong> {{ "{:.1f}".format(stu_trend[-1].get("Stock_to_Use_Ratio_pct", 0)) }}%
{% else %} {% else %}
<strong>--</strong> --
{% endif %} {% endif %}
</p> </div>
<small>Ending stocks / consumption</small> <div class="metric-sub">Ending stocks / consumption</div>
</article> </div>
<article> <div class="metric-card">
<header><small>Trade Balance</small></header> <div class="metric-label">Trade Balance</div>
<p style="font-size: 2rem; margin: 0;"> <div class="metric-value">{{ "{:,.0f}".format(latest.get("Exports", 0) - latest.get("Imports", 0)) }}</div>
<strong>{{ "{:,.0f}".format(latest.get("Exports", 0) - latest.get("Imports", 0)) }}</strong> <div class="metric-sub">Exports minus imports</div>
</p> </div>
<small>Exports minus imports</small>
</article>
<article> <div class="metric-card">
<header><small>Your Plan</small></header> <div class="metric-label">Your Plan</div>
<p style="font-size: 2rem; margin: 0;"><strong>{{ plan | title }}</strong></p> <div class="metric-value">{{ plan | title }}</div>
<small> <div class="metric-sub">
{% if plan == "free" %} {% if plan == "free" %}
<a href="{{ url_for('billing.pricing') }}">Upgrade for full history</a> <a href="{{ url_for('billing.pricing') }}">Upgrade for full history</a>
{% else %} {% else %}
{{ stats.api_calls }} API calls (30d) {{ stats.api_calls }} API calls (30d)
{% endif %} {% endif %}
</small> </div>
</article> </div>
</div> </div>
<!-- Global Supply/Demand Time Series --> <!-- Global Supply/Demand Time Series -->
<section> <div class="chart-container mb-8">
<h2>Global Supply &amp; Demand</h2> <h2 class="text-xl mb-1">Global Supply &amp; Demand</h2>
{% if plan == "free" %} {% if plan == "free" %}
<p><small>Showing last 5 years. <a href="{{ url_for('billing.pricing') }}">Upgrade</a> for full 18+ year history.</small></p> <div class="plan-gate mb-4">Showing last 5 years. <a href="{{ url_for('billing.pricing') }}">Upgrade</a> for full 18+ year history.</div>
{% endif %} {% endif %}
<canvas id="supplyDemandChart" style="max-height: 400px;"></canvas> <canvas id="supplyDemandChart"></canvas>
</section> </div>
<!-- Stock-to-Use Ratio --> <!-- Stock-to-Use Ratio -->
<section> <div class="chart-container mb-8">
<h2>Stock-to-Use Ratio Trend</h2> <h2 class="text-xl mb-4">Stock-to-Use Ratio Trend</h2>
<canvas id="stuChart" style="max-height: 300px;"></canvas> <canvas id="stuChart"></canvas>
</section> </div>
<!-- Two-column: Top Producers + YoY Table --> <!-- Two-column: Top Producers + YoY Table -->
<div class="grid"> <div class="grid-2 mb-8">
<section> <div class="chart-container">
<h2>Top Producing Countries</h2> <h2 class="text-xl mb-4">Top Producing Countries</h2>
<canvas id="topProducersChart" style="max-height: 400px;"></canvas> <canvas id="topProducersChart"></canvas>
</section> </div>
<section> <div class="card">
<h2>Year-over-Year Production Change</h2> <h2 class="text-xl mb-4">Year-over-Year Production Change</h2>
<div style="overflow-x: auto;"> <div class="overflow-x-auto">
<table> <table class="table">
<thead> <thead>
<tr> <tr>
<th>Country</th> <th>Country</th>
<th style="text-align: right;">Production</th> <th class="text-right">Production</th>
<th style="text-align: right;">YoY %</th> <th class="text-right">YoY %</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{% for row in yoy %} {% for row in yoy %}
<tr> <tr>
<td>{{ row.country_name }}</td> <td>{{ row.country_name }}</td>
<td style="text-align: right;">{{ "{:,.0f}".format(row.Production) }}</td> <td class="text-right">{{ "{:,.0f}".format(row.Production) }}</td>
<td style="text-align: right; color: {{ 'var(--ins-color)' if row.Production_YoY_pct and row.Production_YoY_pct > 0 else 'var(--del-color)' if row.Production_YoY_pct and row.Production_YoY_pct < 0 else 'inherit' }};"> <td class="text-right {% if row.Production_YoY_pct and row.Production_YoY_pct > 0 %}text-bean-green{% elif row.Production_YoY_pct and row.Production_YoY_pct < 0 %}text-danger{% endif %}">
{% if row.Production_YoY_pct is not none %} {% if row.Production_YoY_pct is not none %}
{{ "{:+.1f}%".format(row.Production_YoY_pct) }} {{ "{:+.1f}%".format(row.Production_YoY_pct) }}
{% else %} {% else %}
@@ -106,39 +103,58 @@
</tbody> </tbody>
</table> </table>
</div> </div>
</section> </div>
</div> </div>
<!-- CSV Export (plan-gated) --> <!-- CSV Export (plan-gated) -->
{% if plan != "free" %} {% if plan != "free" %}
<section> <div class="mb-8">
<a href="{{ url_for('api.commodity_metrics_csv', code=711100) }}" role="button" class="secondary outline">Export CSV</a> <a href="{{ url_for('api.commodity_metrics_csv', code=711100) }}" class="btn-outline">Export CSV</a>
</section> </div>
{% else %} {% else %}
<section> <div class="plan-gate mb-8">CSV export available on Trader and Analyst plans. <a href="{{ url_for('billing.pricing') }}">Upgrade</a></div>
<p><small>CSV export available on Starter and Pro plans. <a href="{{ url_for('billing.pricing') }}">Upgrade</a></small></p>
</section>
{% endif %} {% endif %}
<!-- Quick Actions --> <!-- Quick Actions -->
<section> <div class="grid-3">
<div class="grid"> <a href="{{ url_for('dashboard.countries') }}" class="btn-outline text-center">Country Comparison</a>
<a href="{{ url_for('dashboard.countries') }}" role="button" class="secondary outline">Country Comparison</a> <a href="{{ url_for('dashboard.settings') }}" class="btn-outline text-center">Settings</a>
<a href="{{ url_for('dashboard.settings') }}" role="button" class="secondary outline">Settings</a> <a href="{{ url_for('dashboard.settings') }}#api-keys" class="btn-outline text-center">API Keys</a>
<a href="{{ url_for('dashboard.settings') }}#api-keys" role="button" class="secondary outline">API Keys</a> </div>
</div>
</section>
</main> </main>
{% endblock %} {% endblock %}
{% block scripts %} {% block scripts %}
<script> <script>
// Chart colors // Brand chart colors
const COLORS = [ const CHART_COLORS = {
'#2563eb', '#dc2626', '#16a34a', '#ca8a04', '#9333ea', copper: '#B45309',
'#0891b2', '#e11d48', '#65a30d', '#d97706', '#7c3aed' roast: '#4A2C1A',
beanGreen: '#15803D',
forest: '#064E3B',
stone: '#78716C',
espresso: '#2C1810',
warning: '#D97706',
danger: '#EF4444',
parchment: '#E8DFD5',
latte: '#F5F0EB',
};
const CHART_PALETTE = [
CHART_COLORS.copper,
CHART_COLORS.beanGreen,
CHART_COLORS.roast,
CHART_COLORS.forest,
CHART_COLORS.stone,
CHART_COLORS.warning,
CHART_COLORS.danger,
CHART_COLORS.espresso,
]; ];
// Chart.js defaults
Chart.defaults.font.family = "'DM Sans', sans-serif";
Chart.defaults.color = CHART_COLORS.stone;
Chart.defaults.borderColor = CHART_COLORS.parchment;
// -- Supply/Demand Chart -- // -- Supply/Demand Chart --
const tsData = {{ time_series | tojson }}; const tsData = {{ time_series | tojson }};
if (tsData.length > 0) { if (tsData.length > 0) {
@@ -147,11 +163,11 @@ if (tsData.length > 0) {
data: { data: {
labels: tsData.map(r => r.market_year), labels: tsData.map(r => r.market_year),
datasets: [ datasets: [
{label: 'Production', data: tsData.map(r => r.Production), borderColor: COLORS[0], tension: 0.3}, {label: 'Production', data: tsData.map(r => r.Production), borderColor: CHART_PALETTE[0], tension: 0.3},
{label: 'Exports', data: tsData.map(r => r.Exports), borderColor: COLORS[1], tension: 0.3}, {label: 'Exports', data: tsData.map(r => r.Exports), borderColor: CHART_PALETTE[1], tension: 0.3},
{label: 'Imports', data: tsData.map(r => r.Imports), borderColor: COLORS[2], tension: 0.3}, {label: 'Imports', data: tsData.map(r => r.Imports), borderColor: CHART_PALETTE[2], tension: 0.3},
{label: 'Ending Stocks', data: tsData.map(r => r.Ending_Stocks), borderColor: COLORS[3], tension: 0.3}, {label: 'Ending Stocks', data: tsData.map(r => r.Ending_Stocks), borderColor: CHART_PALETTE[3], tension: 0.3},
{label: 'Total Distribution', data: tsData.map(r => r.Total_Distribution), borderColor: COLORS[4], tension: 0.3}, {label: 'Total Distribution', data: tsData.map(r => r.Total_Distribution), borderColor: CHART_PALETTE[4], tension: 0.3},
] ]
}, },
options: { options: {
@@ -172,8 +188,8 @@ if (stuData.length > 0) {
datasets: [{ datasets: [{
label: 'Stock-to-Use Ratio (%)', label: 'Stock-to-Use Ratio (%)',
data: stuData.map(r => r.Stock_to_Use_Ratio_pct), data: stuData.map(r => r.Stock_to_Use_Ratio_pct),
borderColor: COLORS[0], borderColor: CHART_COLORS.copper,
backgroundColor: 'rgba(37,99,235,0.1)', backgroundColor: 'rgba(180, 83, 9, 0.08)',
fill: true, fill: true,
tension: 0.3 tension: 0.3
}] }]
@@ -196,7 +212,7 @@ if (topData.length > 0) {
datasets: [{ datasets: [{
label: 'Production', label: 'Production',
data: topData.map(r => r.Production), data: topData.map(r => r.Production),
backgroundColor: COLORS[0] backgroundColor: CHART_COLORS.copper
}] }]
}, },
options: { options: {

View File

@@ -1,75 +1,75 @@
{% extends "base.html" %} {% extends "base.html" %}
{% block title %}Settings - {{ config.APP_NAME }}{% endblock %} {% block title %}Settings {{ config.APP_NAME }}{% endblock %}
{% block content %} {% block content %}
<main class="container"> <main class="container-page py-8">
<header> <!-- Page Header -->
<div class="page-header">
<h1>Settings</h1> <h1>Settings</h1>
</header> </div>
<!-- Profile Section --> <!-- Profile Section -->
<section> <div class="card">
<h2>Profile</h2> <div class="card-header">Profile</div>
<article> <form method="post">
<form method="post"> <input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<div class="mb-4">
<label for="email"> <label for="email" class="form-label">Email</label>
Email <input type="email" id="email" class="form-input bg-latte" value="{{ user.email }}" disabled>
<input type="email" id="email" value="{{ user.email }}" disabled> <p class="form-hint">Email cannot be changed</p>
<small>Email cannot be changed</small> </div>
</label>
<div class="mb-4">
<label for="name"> <label for="name" class="form-label">Name</label>
Name <input type="text" id="name" name="name" class="form-input" value="{{ user.name or '' }}" placeholder="Your name">
<input type="text" id="name" name="name" value="{{ user.name or '' }}" placeholder="Your name"> </div>
</label>
<button type="submit" class="btn">Save Changes</button>
<button type="submit">Save Changes</button> </form>
</form> </div>
</article>
</section>
<!-- Subscription Section --> <!-- Subscription Section -->
<section> <div class="card">
<h2>Subscription</h2> <div class="card-header">Subscription</div>
<article> <div class="grid-3 mb-4">
<div class="grid"> <div>
<div> <span class="text-xs text-stone uppercase tracking-wider">Current Plan</span>
<strong>Current Plan:</strong> {{ (user.plan or 'free') | title }} <p class="font-semibold text-espresso">{{ (user.plan or 'free') | title }}</p>
</div>
<div>
<strong>Status:</strong> {{ (user.sub_status or 'active') | title }}
</div>
{% if user.current_period_end %}
<div>
<strong>Renews:</strong> {{ user.current_period_end[:10] }}
</div>
{% endif %}
</div> </div>
<div>
<div style="margin-top: 1rem;"> <span class="text-xs text-stone uppercase tracking-wider">Status</span>
{% if subscription %} <p class="font-semibold text-espresso">{{ (user.sub_status or 'active') | title }}</p>
<form method="post" action="{{ url_for('billing.portal') }}" style="display: inline;">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<button type="submit" class="secondary">Manage Subscription</button>
</form>
{% else %}
<a href="{{ url_for('billing.pricing') }}" role="button">Upgrade Plan</a>
{% endif %}
</div> </div>
</article> {% if user.current_period_end %}
</section> <div>
<span class="text-xs text-stone uppercase tracking-wider">Renews</span>
<p class="font-semibold text-espresso">{{ user.current_period_end[:10] }}</p>
</div>
{% endif %}
</div>
<div>
{% if subscription %}
<form method="post" action="{{ url_for('billing.portal') }}" class="inline">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<button type="submit" class="btn-secondary">Manage Subscription</button>
</form>
{% else %}
<a href="{{ url_for('billing.pricing') }}" class="btn">Upgrade Plan</a>
{% endif %}
</div>
</div>
<!-- API Keys Section --> <!-- API Keys Section -->
<section id="api-keys"> <div class="card" id="api-keys">
<h2>API Keys</h2> <div class="card-header">API Keys</div>
<article> <p class="text-sm text-stone mb-4">API keys allow you to access the API programmatically.</p>
<p>API keys allow you to access the API programmatically.</p>
{% if api_keys %}
{% if api_keys %} <div class="overflow-x-auto mb-4">
<table> <table class="table">
<thead> <thead>
<tr> <tr>
<th>Name</th> <th>Name</th>
@@ -87,69 +87,71 @@
<td>{{ key.scopes }}</td> <td>{{ key.scopes }}</td>
<td>{{ key.created_at[:10] }}</td> <td>{{ key.created_at[:10] }}</td>
<td> <td>
<form method="post" action="{{ url_for('dashboard.delete_key', key_id=key.id) }}" style="margin: 0;"> <form method="post" action="{{ url_for('dashboard.delete_key', key_id=key.id) }}" class="inline">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"> <input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<button type="submit" class="secondary outline" style="padding: 0.25rem 0.5rem; margin: 0;">Delete</button> <button type="submit" class="btn-danger btn-sm">Delete</button>
</form> </form>
</td> </td>
</tr> </tr>
{% endfor %} {% endfor %}
</tbody> </tbody>
</table> </table>
{% else %} </div>
<p><em>No API keys yet.</em></p> {% else %}
{% endif %} <p class="text-sm text-stone italic mb-4">No API keys yet.</p>
{% endif %}
<details>
<summary>Create New API Key</summary> <details class="faq-item">
<summary>Create New API Key</summary>
<div class="pt-2 pb-4">
<form method="post" action="{{ url_for('dashboard.create_key') }}"> <form method="post" action="{{ url_for('dashboard.create_key') }}">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"> <input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<label for="key-name"> <div class="mb-4">
Key Name <label for="key-name" class="form-label">Key Name</label>
<input type="text" id="key-name" name="name" placeholder="My API Key" required> <input type="text" id="key-name" name="name" class="form-input" placeholder="My API Key" required>
</label> </div>
<fieldset> <div class="mb-4">
<legend>Scopes</legend> <span class="form-label">Scopes</span>
<label> <div class="flex gap-4 mt-1">
<input type="checkbox" name="scopes" value="read" checked> <label class="flex items-center gap-2 text-sm text-stone-dark">
Read <input type="checkbox" name="scopes" value="read" checked class="accent-copper">
</label> Read
<label> </label>
<input type="checkbox" name="scopes" value="write"> <label class="flex items-center gap-2 text-sm text-stone-dark">
Write <input type="checkbox" name="scopes" value="write" class="accent-copper">
</label> Write
</fieldset> </label>
</div>
<button type="submit">Create Key</button> </div>
<button type="submit" class="btn">Create Key</button>
</form> </form>
</details> </div>
</article> </details>
</section> </div>
<!-- Danger Zone --> <!-- Danger Zone -->
<section> <div class="danger-card">
<h2>Danger Zone</h2> <h2 class="text-xl text-danger mb-2">Danger Zone</h2>
<article style="border-color: var(--del-color);"> <p class="text-sm text-stone mb-4">Once you delete your account, there is no going back. Please be certain.</p>
<p>Once you delete your account, there is no going back. Please be certain.</p>
<details class="faq-item border-danger/30">
<details> <summary class="text-danger">Delete Account</summary>
<summary role="button" class="secondary outline" style="--pico-color: var(--del-color);">Delete Account</summary> <div class="pt-2 pb-4">
<p>Are you sure? This will:</p> <p class="text-sm text-stone mb-2">Are you sure? This will:</p>
<ul> <ul class="list-disc pl-5 space-y-1 text-sm text-stone mb-4">
<li>Delete all your data</li> <li>Delete all your data</li>
<li>Cancel your subscription</li> <li>Cancel your subscription</li>
<li>Remove your API keys</li> <li>Remove your API keys</li>
</ul> </ul>
<form method="post" action="{{ url_for('dashboard.delete_account') }}"> <form method="post" action="{{ url_for('dashboard.delete_account') }}">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"> <input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<button type="submit" class="secondary" style="--pico-background-color: var(--del-color);"> <button type="submit" class="btn-danger">Yes, Delete My Account</button>
Yes, Delete My Account
</button>
</form> </form>
</details> </div>
</article> </details>
</section> </div>
</main> </main>
{% endblock %} {% endblock %}

View File

@@ -1,34 +1,48 @@
{% extends "base.html" %} {% extends "base.html" %}
{% block title %}About - {{ config.APP_NAME }}{% endblock %} {% block title %}About {{ config.APP_NAME }}{% endblock %}
{% block content %} {% block content %}
<main class="container"> <main>
<article style="max-width: 800px; margin: 0 auto;"> <!-- Hero -->
<header style="text-align: center;"> <section class="hero">
<h1>About {{ config.APP_NAME }}</h1> <div class="container-page">
</header> <h1 class="heading-display">Coffee data should be accessible to everyone who trades it</h1>
<p>BeanFlows exists because the gap between Bloomberg and free USDA flat files shouldn't be $24,000.</p>
<section> </div>
<p>{{ config.APP_NAME }} was built with a simple philosophy: ship fast, stay simple.</p> </section>
<p>Too many SaaS boilerplates are over-engineered. They use PostgreSQL when SQLite would do. They add Redis for a job queue that runs 10 tasks a day. They have 50 npm dependencies for a landing page.</p> <!-- Mission -->
<section class="container-page py-12 max-w-3xl mx-auto">
<p>We took a different approach:</p> <h2 class="text-2xl mb-4">The problem</h2>
<p class="text-stone mb-4">If you're an independent coffee trader, roaster, or analyst, your data options are limited. You either pay for a Bloomberg Terminal you'll use 5% of, spend hours manually downloading and wrangling USDA flat files, or subscribe to a platform built for hedge funds trading 50 commodities at once.</p>
<ul> <p class="text-stone mb-8">None of these are built for you.</p>
<li><strong>SQLite for everything</strong> It handles more than you think.</li>
<li><strong>Server-rendered HTML</strong> No build step, no hydration, no complexity.</li> <h2 class="text-2xl mb-4">What BeanFlows does</h2>
<li><strong>Minimal dependencies</strong> Fewer things to break.</li> <p class="text-stone mb-4">We take the freely available USDA Production, Supply &amp; Distribution data &mdash; the same data the big terminals use &mdash; and transform it into clean, analytics-ready metrics. Charts, country comparisons, stock-to-use ratios, CSV exports, and a REST API. Updated daily.</p>
<li><strong>Flat structure</strong> Find things where you expect them.</li> <p class="text-stone mb-8">No proprietary black box. The source data is public. Our methodology is transparent.</p>
</ul>
<h2 class="text-2xl mb-4">How we build it</h2>
<p>The result is a codebase you can understand in an afternoon and deploy for $5/month.</p> <p class="text-stone mb-4">Our data pipeline runs daily: extract from USDA, transform through a 4-layer SQL pipeline (raw, staging, cleaned, serving), and deliver to the dashboard. Built with DuckDB, SQLMesh, and Python. No heavy infrastructure, no unnecessary complexity.</p>
</section>
<h2 class="text-2xl mb-4">Who this is for</h2>
<section style="text-align: center; margin-top: 3rem;"> <ul class="space-y-2 text-stone mb-8">
<a href="{{ url_for('auth.signup') }}" role="button">Get Started</a> <li><strong class="text-espresso">Independent traders</strong> who need coffee supply &amp; demand data without a terminal subscription.</li>
</section> <li><strong class="text-espresso">Roasters &amp; importers</strong> tracking origin country production trends for sourcing decisions.</li>
</article> <li><strong class="text-espresso">Analysts &amp; researchers</strong> who want clean, exportable coffee commodity data.</li>
</ul>
<h2 class="text-2xl mb-4">The team</h2>
<p class="text-stone mb-8">BeanFlows is built and operated by a solo developer who believes commodity data should be simple, affordable, and transparent. No sales team, no enterprise negotiations. Just data.</p>
</section>
<!-- CTA -->
<section class="bg-latte py-16">
<div class="container-page text-center">
<h2 class="heading-display text-3xl md:text-4xl mb-4">See the data for yourself</h2>
<p class="text-stone text-lg max-w-xl mx-auto mb-8">Free plan. Real data. No credit card.</p>
<a href="{{ url_for('auth.signup') }}" class="btn">Start free</a>
</div>
</section>
</main> </main>
{% endblock %} {% endblock %}

View File

@@ -1,73 +1,129 @@
{% extends "base.html" %} {% extends "base.html" %}
{% block title %}Features - {{ config.APP_NAME }}{% endblock %} {% block title %}Features {{ config.APP_NAME }}{% endblock %}
{% block content %} {% block content %}
<main class="container"> <main>
<header style="text-align: center; margin-bottom: 3rem;"> <!-- Hero -->
<h1>Features</h1> <section class="hero">
<p>Coffee market intelligence built on USDA Production, Supply &amp; Distribution data.</p> <div class="container-page">
</header> <h1 class="heading-display">Built for coffee traders, not Wall Street</h1>
<p>Every feature exists because an independent trader or roaster needed it.</p>
<section> </div>
<article>
<h2>Supply &amp; Demand Dashboard</h2>
<p>Interactive charts showing global coffee production, exports, imports, ending stocks, and total distribution by market year. Spot surplus and deficit years at a glance.</p>
<ul>
<li>18+ years of historical data (2006&ndash;present)</li>
<li>Line charts for production, trade, and consumption trends</li>
<li>Key metric cards for quick orientation</li>
<li>Auto-refreshed daily from USDA PSD Online</li>
</ul>
</article>
<article>
<h2>Country Analysis &amp; Comparison</h2>
<p>Rank the world's coffee producers and consumers. Compare up to 10 countries side-by-side on any metric.</p>
<ul>
<li>Top-N country rankings (production, exports, imports, stocks)</li>
<li>Year-over-year production change table with directional coloring</li>
<li>Multi-country overlay charts</li>
<li>65 commodity-country combinations from USDA data</li>
</ul>
</article>
<article>
<h2>Stock-to-Use Ratio</h2>
<p>The ratio traders watch most closely. Track the global coffee stock-to-use ratio over time to gauge market tightness and anticipate price moves.</p>
<ul>
<li>Global ratio trend chart</li>
<li>Ending stocks vs. total distribution breakdown</li>
<li>Historical context spanning two decades</li>
</ul>
</article>
<article>
<h2>Data Export &amp; API</h2>
<p>Download CSV files or integrate directly with your trading systems via REST API.</p>
<ul>
<li>CSV export of any metric series</li>
<li>RESTful JSON API with Bearer token auth</li>
<li>Rate-limited and logged for security</li>
<li>Commodity listing, time series, and country endpoints</li>
</ul>
</article>
<article>
<h2>Daily Data Pipeline</h2>
<p>Our pipeline extracts data from the USDA PSD Online database, transforms it through a 4-layer SQL pipeline (raw &rarr; staging &rarr; cleaned &rarr; serving), and delivers analytics-ready metrics every day.</p>
<ul>
<li>Automated daily extraction from USDA</li>
<li>SQLMesh + DuckDB transformation pipeline</li>
<li>Incremental processing (only new data each day)</li>
<li>Auditable data lineage</li>
</ul>
</article>
</section> </section>
<section style="text-align: center; margin-top: 3rem;"> <!-- Feature 1: Supply & Demand -->
<a href="{{ url_for('auth.signup') }}" role="button">Start Free</a> <section class="container-page py-12">
<a href="{{ url_for('billing.pricing') }}" role="button" class="secondary outline" style="margin-left: 1rem;">View Pricing</a> <div class="grid-2 items-center">
<div>
<h2 class="text-2xl md:text-3xl mb-4">Supply &amp; Demand Dashboard</h2>
<p class="text-stone mb-4">Interactive charts showing global coffee production, exports, imports, ending stocks, and total distribution by market year. Spot surplus and deficit years at a glance.</p>
<ul class="space-y-2 text-sm text-stone">
<li>&#10003; 18+ years of historical data (2006&ndash;present)</li>
<li>&#10003; Line charts for production, trade, and consumption trends</li>
<li>&#10003; Key metric cards for quick orientation</li>
<li>&#10003; Auto-refreshed daily from USDA PSD Online</li>
</ul>
</div>
<div class="bg-latte rounded-2xl h-64 flex items-center justify-center text-stone">
<span class="text-sm">Supply &amp; Demand chart preview</span>
</div>
</div>
</section>
<hr class="container-page">
<!-- Feature 2: Country Analysis -->
<section class="container-page py-12">
<div class="grid-2 items-center">
<div class="order-2 md:order-1 bg-latte rounded-2xl h-64 flex items-center justify-center text-stone">
<span class="text-sm">Country comparison preview</span>
</div>
<div class="order-1 md:order-2">
<h2 class="text-2xl md:text-3xl mb-4">Country Analysis &amp; Comparison</h2>
<p class="text-stone mb-4">Rank the world's coffee producers and consumers. Compare up to 10 countries side-by-side on any metric.</p>
<ul class="space-y-2 text-sm text-stone">
<li>&#10003; Top-N country rankings (production, exports, imports, stocks)</li>
<li>&#10003; Year-over-year production change with directional coloring</li>
<li>&#10003; Multi-country overlay charts</li>
<li>&#10003; 65 commodity-country combinations from USDA data</li>
</ul>
</div>
</div>
</section>
<hr class="container-page">
<!-- Feature 3: Stock-to-Use -->
<section class="container-page py-12">
<div class="grid-2 items-center">
<div>
<h2 class="text-2xl md:text-3xl mb-4">Stock-to-Use Ratio</h2>
<p class="text-stone mb-4">The ratio traders watch most closely. Track the global coffee stock-to-use ratio over time to gauge market tightness and anticipate price moves.</p>
<ul class="space-y-2 text-sm text-stone">
<li>&#10003; Global ratio trend chart spanning two decades</li>
<li>&#10003; Ending stocks vs. total distribution breakdown</li>
<li>&#10003; Historical context for current market conditions</li>
</ul>
</div>
<div class="bg-latte rounded-2xl h-64 flex items-center justify-center text-stone">
<span class="text-sm">Stock-to-use ratio preview</span>
</div>
</div>
</section>
<hr class="container-page">
<!-- Feature 4: Export & API -->
<section class="container-page py-12">
<div class="grid-2 items-center">
<div class="order-2 md:order-1 bg-latte rounded-2xl h-64 flex items-center justify-center text-stone">
<span class="text-sm">API response preview</span>
</div>
<div class="order-1 md:order-2">
<h2 class="text-2xl md:text-3xl mb-4">Data Export &amp; API Access</h2>
<p class="text-stone mb-4">Download CSV files or integrate directly with your trading systems via REST API.</p>
<ul class="space-y-2 text-sm text-stone">
<li>&#10003; CSV export of any metric series</li>
<li>&#10003; RESTful JSON API with Bearer token auth</li>
<li>&#10003; Rate-limited and logged for security</li>
<li>&#10003; Commodity listing, time series, and country endpoints</li>
</ul>
</div>
</div>
</section>
<hr class="container-page">
<!-- Feature 5: Pipeline -->
<section class="container-page py-12">
<div class="grid-2 items-center">
<div>
<h2 class="text-2xl md:text-3xl mb-4">Daily Automated Pipeline</h2>
<p class="text-stone mb-4">Our pipeline extracts data from USDA PSD Online, transforms it through a 4-layer SQL pipeline, and delivers analytics-ready metrics every day.</p>
<ul class="space-y-2 text-sm text-stone">
<li>&#10003; Automated daily extraction from USDA</li>
<li>&#10003; SQLMesh + DuckDB transformation pipeline</li>
<li>&#10003; Incremental processing (only new data each day)</li>
<li>&#10003; Auditable data lineage</li>
</ul>
</div>
<div class="bg-latte rounded-2xl h-64 flex items-center justify-center text-stone">
<span class="text-sm">Pipeline diagram preview</span>
</div>
</div>
</section>
<!-- CTA -->
<section class="bg-latte py-16">
<div class="container-page text-center">
<h2 class="heading-display text-3xl md:text-4xl mb-4">Start with the free plan</h2>
<p class="text-stone text-lg max-w-xl mx-auto mb-8">Real data, not a demo. 5 years of coffee market data, charts, and metrics.</p>
<div class="flex flex-col sm:flex-row gap-3 justify-center">
<a href="{{ url_for('auth.signup') }}" class="btn">Start free</a>
<a href="{{ url_for('billing.pricing') }}" class="btn-outline">View pricing</a>
</div>
</div>
</section> </section>
</main> </main>
{% endblock %} {% endblock %}

View File

@@ -1,91 +1,118 @@
{% extends "base.html" %} {% extends "base.html" %}
{% block title %}{{ config.APP_NAME }} - Coffee Market Intelligence for Independent Traders{% endblock %} {% block title %}{{ config.APP_NAME }} Coffee Market Data Without the Bloomberg Price Tag{% endblock %}
{% block content %} {% block content %}
<main class="container"> <main>
<!-- Hero --> <!-- Hero -->
<header style="text-align: center; padding: 4rem 0;"> <section class="hero">
<h1>Coffee Market Intelligence<br>for Independent Traders</h1> <div class="container-page">
<p style="font-size: 1.25rem; max-width: 640px; margin: 0 auto;"> <h1 class="heading-display">Coffee market data without the Bloomberg price tag</h1>
Track global supply and demand, compare producing countries, and spot trends <p>18 years of USDA production, trade, and stock data for every coffee-producing country. Charts, exports, and API. Updated daily.</p>
with 18+ years of USDA data. No expensive terminal required. <div class="flex flex-col sm:flex-row gap-3 justify-center">
</p> <a href="{{ url_for('auth.signup') }}" class="btn">Start free &mdash; no credit card</a>
<div style="margin-top: 2rem;"> <a href="{{ url_for('public.features') }}" class="btn-outline">See what's included</a>
<a href="{{ url_for('auth.signup') }}" role="button" style="margin-right: 1rem;">Start Free</a> </div>
<a href="{{ url_for('public.features') }}" role="button" class="secondary outline">See Features</a>
</div> </div>
</header> </section>
<!-- Problem -->
<section class="container-page py-16">
<div class="section-heading">
<h2>Good coffee data shouldn't cost $24,000 a year</h2>
<p>Independent traders face bad options for getting the data they need.</p>
</div>
<div class="grid-3">
<div class="feature-card text-center">
<div class="feature-icon">&#128176;</div>
<h3>Bloomberg Terminal</h3>
<p>$24,000/yr &mdash; Overkill for most independent traders.</p>
</div>
<div class="feature-card text-center">
<div class="feature-icon">&#128196;</div>
<h3>Manual USDA Digging</h3>
<p>$0 but hours of work &mdash; No charts, no API, just flat files.</p>
</div>
<div class="feature-card text-center">
<div class="feature-icon">&#127970;</div>
<h3>Other Platforms</h3>
<p>$200&ndash;500/mo &mdash; Built for hedge funds trading 50 commodities.</p>
</div>
</div>
</section>
<!-- Value Props --> <!-- Value Props -->
<section style="padding: 4rem 0;"> <section class="bg-latte py-16">
<h2 style="text-align: center;">What You Get</h2> <div class="container-page">
<div class="section-heading">
<div class="grid"> <h2>What you get</h2>
<article> </div>
<h3>Supply &amp; Demand Charts</h3> <div class="grid-3">
<p>Global production, exports, imports, ending stocks, and consumption visualized by market year.</p> <div class="feature-card">
</article> <div class="feature-icon">&#128200;</div>
<h3>Supply &amp; Demand Charts</h3>
<article> <p>18+ years of production, exports, imports, and ending stocks. Interactive and filterable by market year.</p>
<h3>Country Analysis</h3> </div>
<p>Compare up to 10 producing countries side-by-side. See who's growing, who's shrinking.</p> <div class="feature-card">
</article> <div class="feature-icon">&#127758;</div>
<h3>Country Comparison</h3>
<article> <p>Compare up to 10 producing countries side-by-side on any metric. See who's growing, who's shrinking.</p>
<h3>Stock-to-Use Ratio</h3> </div>
<p>The key indicator traders watch. Track the global ratio over time to gauge tightness.</p> <div class="feature-card">
</article> <div class="feature-icon">&#128202;</div>
</div> <h3>Stock-to-Use Ratio</h3>
<p>The ratio that moves prices. Track the global coffee stock-to-use ratio to gauge market tightness.</p>
<div class="grid"> </div>
<article> <div class="feature-card">
<h3>CSV &amp; API Export</h3> <div class="feature-icon">&#128229;</div>
<p>Download data for your own models. Integrate with your trading tools via REST API.</p> <h3>CSV &amp; API Export</h3>
</article> <p>Your data, your format. Download CSV files or integrate directly with your trading systems via REST API.</p>
</div>
<article> <div class="feature-card">
<h3>Daily Refresh</h3> <div class="feature-icon">&#9889;</div>
<p>Data pipeline runs daily against USDA PSD Online. Always current, always reliable.</p> <h3>Daily Refresh</h3>
</article> <p>Our pipeline runs daily against USDA PSD Online. Latest data within hours of USDA publishing.</p>
</div>
<article> <div class="feature-card">
<h3>No Lock-in</h3> <div class="feature-icon">&#128275;</div>
<p>Public USDA data, open methodology. You own your exports. Cancel anytime.</p> <h3>Public Data, Open Method</h3>
</article> <p>No black box. Built on freely available USDA data with a fully transparent transformation pipeline.</p>
</div>
</div>
</div> </div>
</section> </section>
<!-- How It Works --> <!-- How It Works -->
<section style="background: var(--card-background-color); border-radius: var(--border-radius); padding: 2rem;"> <section class="container-page py-16">
<h2 style="text-align: center;">How It Works</h2> <div class="section-heading">
<h2>How it works</h2>
<div class="grid"> </div>
<div style="text-align: center;"> <div class="grid-3 text-center">
<p style="font-size: 2rem;">1</p> <div>
<h4>Sign Up</h4> <div class="step-number">1</div>
<p>Enter your email, click the magic link. No password needed.</p> <h4 class="mb-2">Sign up with your email</h4>
<p class="text-sm text-stone">Magic link, no password. You're in within seconds.</p>
</div> </div>
<div>
<div style="text-align: center;"> <div class="step-number">2</div>
<p style="font-size: 2rem;">2</p> <h4 class="mb-2">Explore the dashboard</h4>
<h4>Explore the Dashboard</h4> <p class="text-sm text-stone">Charts and metrics are ready. No setup, no configuration.</p>
<p>Instant access to coffee supply/demand charts and country rankings.</p>
</div> </div>
<div>
<div style="text-align: center;"> <div class="step-number">3</div>
<p style="font-size: 2rem;">3</p> <h4 class="mb-2">Export or connect via API</h4>
<h4>Go Deeper</h4> <p class="text-sm text-stone">CSV downloads or REST API for your own models and tools.</p>
<p>Upgrade for full history, CSV exports, and API access for your own models.</p>
</div> </div>
</div> </div>
</section> </section>
<!-- CTA --> <!-- Final CTA -->
<section style="text-align: center; padding: 4rem 0;"> <section class="bg-latte py-16">
<h2>Ready to See the Data?</h2> <div class="container-page text-center">
<p>Free plan includes the last 5 years of coffee market data. No credit card required.</p> <h2 class="heading-display text-3xl md:text-4xl mb-4">See the data before you decide</h2>
<a href="{{ url_for('auth.signup') }}" role="button">Start Free</a> <p class="text-stone text-lg max-w-xl mx-auto mb-8">Free plan includes 5 years of coffee market data. No credit card. No sales call. Just data.</p>
<a href="{{ url_for('auth.signup') }}" class="btn">Start free</a>
</div>
</section> </section>
</main> </main>
{% endblock %} {% endblock %}

View File

@@ -1,92 +1,94 @@
{% extends "base.html" %} {% extends "base.html" %}
{% block title %}Privacy Policy - {{ config.APP_NAME }}{% endblock %} {% block title %}Privacy Policy {{ config.APP_NAME }}{% endblock %}
{% block content %} {% block content %}
<main class="container"> <main class="container-page py-12">
<article style="max-width: 800px; margin: 0 auto;"> <div class="max-w-3xl mx-auto">
<header> <div class="page-header">
<h1>Privacy Policy</h1> <h1>Privacy Policy</h1>
<p><small>Last updated: January 2024</small></p> <p class="text-sm text-stone">Last updated: January 2024</p>
</header> </div>
<section> <div class="card">
<h2>1. Information We Collect</h2> <section class="mb-8">
<p>We collect information you provide directly:</p> <h2 class="text-xl mb-3">1. Information We Collect</h2>
<ul> <p class="text-stone mb-2">We collect information you provide directly:</p>
<li>Email address (required for account creation)</li> <ul class="list-disc pl-5 space-y-1 text-sm text-stone mb-3">
<li>Name (optional)</li> <li>Email address (required for account creation)</li>
<li>Payment information (processed by Stripe)</li> <li>Name (optional)</li>
</ul> <li>Payment information (processed by Paddle)</li>
<p>We automatically collect:</p> </ul>
<ul> <p class="text-stone mb-2">We automatically collect:</p>
<li>IP address</li> <ul class="list-disc pl-5 space-y-1 text-sm text-stone">
<li>Browser type</li> <li>IP address</li>
<li>Usage data</li> <li>Browser type</li>
</ul> <li>Usage data</li>
</section> </ul>
</section>
<section>
<h2>2. How We Use Information</h2> <section class="mb-8">
<p>We use your information to:</p> <h2 class="text-xl mb-3">2. How We Use Information</h2>
<ul> <p class="text-stone mb-2">We use your information to:</p>
<li>Provide and maintain the service</li> <ul class="list-disc pl-5 space-y-1 text-sm text-stone">
<li>Process payments</li> <li>Provide and maintain the service</li>
<li>Send transactional emails</li> <li>Process payments</li>
<li>Improve the service</li> <li>Send transactional emails</li>
<li>Respond to support requests</li> <li>Improve the service</li>
</ul> <li>Respond to support requests</li>
</section> </ul>
</section>
<section>
<h2>3. Information Sharing</h2> <section class="mb-8">
<p>We do not sell your personal information. We may share information with:</p> <h2 class="text-xl mb-3">3. Information Sharing</h2>
<ul> <p class="text-stone mb-2">We do not sell your personal information. We may share information with:</p>
<li>Service providers (Stripe for payments, Resend for email)</li> <ul class="list-disc pl-5 space-y-1 text-sm text-stone">
<li>Law enforcement when required by law</li> <li>Service providers (Paddle for payments, Resend for email)</li>
</ul> <li>Law enforcement when required by law</li>
</section> </ul>
</section>
<section>
<h2>4. Data Retention</h2> <section class="mb-8">
<p>We retain your data as long as your account is active. Upon deletion, we remove your data within 30 days.</p> <h2 class="text-xl mb-3">4. Data Retention</h2>
</section> <p class="text-stone">We retain your data as long as your account is active. Upon deletion, we remove your data within 30 days.</p>
</section>
<section>
<h2>5. Security</h2> <section class="mb-8">
<p>We implement industry-standard security measures including encryption, secure sessions, and regular backups.</p> <h2 class="text-xl mb-3">5. Security</h2>
</section> <p class="text-stone">We implement industry-standard security measures including encryption, secure sessions, and regular backups.</p>
</section>
<section>
<h2>6. Cookies</h2> <section class="mb-8">
<p>We use essential cookies for session management. We do not use tracking or advertising cookies.</p> <h2 class="text-xl mb-3">6. Cookies</h2>
</section> <p class="text-stone">We use essential cookies for session management. We do not use tracking or advertising cookies.</p>
</section>
<section>
<h2>7. Your Rights</h2> <section class="mb-8">
<p>You have the right to:</p> <h2 class="text-xl mb-3">7. Your Rights</h2>
<ul> <p class="text-stone mb-2">You have the right to:</p>
<li>Access your data</li> <ul class="list-disc pl-5 space-y-1 text-sm text-stone">
<li>Correct inaccurate data</li> <li>Access your data</li>
<li>Delete your account and data</li> <li>Correct inaccurate data</li>
<li>Export your data</li> <li>Delete your account and data</li>
</ul> <li>Export your data</li>
</section> </ul>
</section>
<section>
<h2>8. GDPR Compliance</h2> <section class="mb-8">
<p>For EU users: We process data based on consent and legitimate interest. You may contact us to exercise your GDPR rights.</p> <h2 class="text-xl mb-3">8. GDPR Compliance</h2>
</section> <p class="text-stone">For EU users: We process data based on consent and legitimate interest. You may contact us to exercise your GDPR rights.</p>
</section>
<section>
<h2>9. Changes</h2> <section class="mb-8">
<p>We may update this policy. We will notify you of significant changes via email.</p> <h2 class="text-xl mb-3">9. Changes</h2>
</section> <p class="text-stone">We may update this policy. We will notify you of significant changes via email.</p>
</section>
<section>
<h2>10. Contact</h2> <section>
<p>For privacy inquiries: {{ config.EMAIL_FROM }}</p> <h2 class="text-xl mb-3">10. Contact</h2>
</section> <p class="text-stone">For privacy inquiries: {{ config.EMAIL_FROM }}</p>
</article> </section>
</div>
</div>
</main> </main>
{% endblock %} {% endblock %}

View File

@@ -1,71 +1,73 @@
{% extends "base.html" %} {% extends "base.html" %}
{% block title %}Terms of Service - {{ config.APP_NAME }}{% endblock %} {% block title %}Terms of Service {{ config.APP_NAME }}{% endblock %}
{% block content %} {% block content %}
<main class="container"> <main class="container-page py-12">
<article style="max-width: 800px; margin: 0 auto;"> <div class="max-w-3xl mx-auto">
<header> <div class="page-header">
<h1>Terms of Service</h1> <h1>Terms of Service</h1>
<p><small>Last updated: January 2024</small></p> <p class="text-sm text-stone">Last updated: January 2024</p>
</header> </div>
<section> <div class="card">
<h2>1. Acceptance of Terms</h2> <section class="mb-8">
<p>By accessing or using {{ config.APP_NAME }}, you agree to be bound by these Terms of Service. If you do not agree, do not use the service.</p> <h2 class="text-xl mb-3">1. Acceptance of Terms</h2>
</section> <p class="text-stone">By accessing or using {{ config.APP_NAME }}, you agree to be bound by these Terms of Service. If you do not agree, do not use the service.</p>
</section>
<section>
<h2>2. Description of Service</h2> <section class="mb-8">
<p>{{ config.APP_NAME }} provides a software-as-a-service platform. Features and functionality may change over time.</p> <h2 class="text-xl mb-3">2. Description of Service</h2>
</section> <p class="text-stone">{{ config.APP_NAME }} provides a software-as-a-service platform. Features and functionality may change over time.</p>
</section>
<section>
<h2>3. User Accounts</h2> <section class="mb-8">
<p>You are responsible for maintaining the security of your account. You must provide accurate information and keep it updated.</p> <h2 class="text-xl mb-3">3. User Accounts</h2>
</section> <p class="text-stone">You are responsible for maintaining the security of your account. You must provide accurate information and keep it updated.</p>
</section>
<section>
<h2>4. Acceptable Use</h2> <section class="mb-8">
<p>You agree not to:</p> <h2 class="text-xl mb-3">4. Acceptable Use</h2>
<ul> <p class="text-stone mb-2">You agree not to:</p>
<li>Violate any laws or regulations</li> <ul class="list-disc pl-5 space-y-1 text-sm text-stone">
<li>Infringe on intellectual property rights</li> <li>Violate any laws or regulations</li>
<li>Transmit harmful code or malware</li> <li>Infringe on intellectual property rights</li>
<li>Attempt to gain unauthorized access</li> <li>Transmit harmful code or malware</li>
<li>Interfere with service operation</li> <li>Attempt to gain unauthorized access</li>
</ul> <li>Interfere with service operation</li>
</section> </ul>
</section>
<section>
<h2>5. Payment Terms</h2> <section class="mb-8">
<p>Paid plans are billed in advance. Refunds are handled on a case-by-case basis. We may change pricing with 30 days notice.</p> <h2 class="text-xl mb-3">5. Payment Terms</h2>
</section> <p class="text-stone">Paid plans are billed in advance. Refunds are handled on a case-by-case basis. We may change pricing with 30 days notice.</p>
</section>
<section>
<h2>6. Termination</h2> <section class="mb-8">
<p>We may terminate or suspend your account for violations of these terms. You may cancel your account at any time.</p> <h2 class="text-xl mb-3">6. Termination</h2>
</section> <p class="text-stone">We may terminate or suspend your account for violations of these terms. You may cancel your account at any time.</p>
</section>
<section>
<h2>7. Disclaimer of Warranties</h2> <section class="mb-8">
<p>The service is provided "as is" without warranties of any kind. We do not guarantee uninterrupted or error-free operation.</p> <h2 class="text-xl mb-3">7. Disclaimer of Warranties</h2>
</section> <p class="text-stone">The service is provided "as is" without warranties of any kind. We do not guarantee uninterrupted or error-free operation.</p>
</section>
<section>
<h2>8. Limitation of Liability</h2> <section class="mb-8">
<p>We shall not be liable for any indirect, incidental, special, or consequential damages arising from use of the service.</p> <h2 class="text-xl mb-3">8. Limitation of Liability</h2>
</section> <p class="text-stone">We shall not be liable for any indirect, incidental, special, or consequential damages arising from use of the service.</p>
</section>
<section>
<h2>9. Changes to Terms</h2> <section class="mb-8">
<p>We may modify these terms at any time. Continued use after changes constitutes acceptance of the new terms.</p> <h2 class="text-xl mb-3">9. Changes to Terms</h2>
</section> <p class="text-stone">We may modify these terms at any time. Continued use after changes constitutes acceptance of the new terms.</p>
</section>
<section>
<h2>10. Contact</h2> <section>
<p>For questions about these terms, please contact us at {{ config.EMAIL_FROM }}.</p> <h2 class="text-xl mb-3">10. Contact</h2>
</section> <p class="text-stone">For questions about these terms, please contact us at {{ config.EMAIL_FROM }}.</p>
</article> </section>
</div>
</div>
</main> </main>
{% endblock %} {% endblock %}

View File

@@ -1,40 +0,0 @@
/* BeanFlows Custom Styles */
article {
margin-bottom: 1.5rem;
}
code {
background: var(--code-background-color);
padding: 0.125rem 0.25rem;
border-radius: var(--border-radius);
}
table {
width: 100%;
}
/* HTMX loading indicators */
.htmx-indicator {
display: none;
}
.htmx-request .htmx-indicator {
display: inline;
}
.htmx-request.htmx-indicator {
display: inline;
}
/* Dashboard chart sections */
section canvas {
width: 100% !important;
}
/* Key metric cards */
article header small {
text-transform: uppercase;
letter-spacing: 0.05em;
color: var(--muted-color);
}

View File

@@ -2,7 +2,7 @@
/* ── BeanFlows Brand Theme ── */ /* ── BeanFlows Brand Theme ── */
@theme { @theme {
--font-display: "DM Sans", ui-sans-serif, system-ui, sans-serif; --font-display: "Fraunces", "DM Sans", ui-serif, Georgia, serif;
--font-sans: "DM Sans", ui-sans-serif, system-ui, -apple-system, sans-serif; --font-sans: "DM Sans", ui-sans-serif, system-ui, -apple-system, sans-serif;
--font-mono: ui-monospace, "Cascadia Code", monospace; --font-mono: ui-monospace, "Cascadia Code", monospace;
@@ -118,9 +118,34 @@
padding: 0; padding: 0;
display: inline; display: inline;
} }
.nav-hamburger {
display: none;
background: none;
border: none;
cursor: pointer;
padding: 4px;
color: var(--color-espresso);
}
@media (max-width: 768px) { @media (max-width: 768px) {
.nav-links { display: none; } .nav-hamburger { display: block; }
.nav-inner { justify-content: center; } .nav-links {
display: none;
position: absolute;
top: 56px;
left: 0;
right: 0;
flex-direction: column;
background: rgba(255, 251, 245, 0.97);
backdrop-filter: blur(14px);
-webkit-backdrop-filter: blur(14px);
border-bottom: 1px solid rgba(232, 223, 213, 0.7);
padding: 1rem;
gap: 0.75rem;
align-items: stretch;
text-align: center;
}
.nav-links.open { display: flex; }
.nav-inner { position: relative; }
} }
/* Page container */ /* Page container */
@@ -128,6 +153,148 @@
@apply max-w-6xl mx-auto px-4 sm:px-6 lg:px-8; @apply max-w-6xl mx-auto px-4 sm:px-6 lg:px-8;
} }
/* ── Display headings (Fraunces serif) ── */
.heading-display {
font-family: var(--font-display);
@apply text-espresso font-bold tracking-tight;
}
/* ── Section heading (centered title + subtitle) ── */
.section-heading {
@apply text-center mb-12;
}
.section-heading h2 {
@apply text-3xl md:text-4xl mb-3;
}
.section-heading p {
@apply text-stone text-lg max-w-2xl mx-auto;
}
/* ── Hero section ── */
.hero {
@apply text-center py-20 md:py-28;
}
.hero h1 {
font-family: var(--font-display);
@apply text-4xl md:text-5xl lg:text-6xl mb-6 max-w-4xl mx-auto leading-tight;
}
.hero p {
@apply text-lg md:text-xl text-stone max-w-2xl mx-auto mb-8;
}
/* ── Feature card (icon + heading + text) ── */
.feature-card {
@apply bg-white border border-parchment rounded-2xl p-6 shadow-sm
hover:shadow-md transition-shadow;
}
.feature-card .feature-icon {
@apply text-3xl mb-3;
}
.feature-card h3 {
@apply text-lg mb-2;
}
.feature-card p {
@apply text-sm text-stone leading-relaxed;
}
/* ── Metric card (dashboard KPIs) ── */
.metric-card {
@apply bg-white border border-parchment rounded-2xl p-5 shadow-sm;
}
.metric-label {
@apply text-xs font-semibold text-stone uppercase tracking-wider;
}
.metric-value {
@apply text-2xl md:text-3xl font-bold text-espresso font-mono mt-1;
}
.metric-sub {
@apply text-xs text-stone mt-1;
}
/* ── Auth card (narrow centered) ── */
.auth-card {
@apply bg-white border border-parchment rounded-2xl p-8 shadow-sm
max-w-md mx-auto my-16;
}
.auth-card h1 {
@apply text-2xl mb-2;
}
.auth-card p.subtitle {
@apply text-stone text-sm mb-6;
}
/* ── Pricing cards ── */
.pricing-card {
@apply bg-white border border-parchment rounded-2xl p-6 shadow-sm
flex flex-col;
}
.pricing-card-highlighted {
@apply bg-white border-2 border-copper rounded-2xl p-6 shadow-md
flex flex-col relative;
}
.pricing-badge {
@apply absolute -top-3 left-1/2 -translate-x-1/2
bg-copper text-white text-xs font-semibold px-3 py-1 rounded-full;
}
/* ── Step number (how-it-works) ── */
.step-number {
@apply w-10 h-10 rounded-full bg-copper text-white
flex items-center justify-center text-sm font-bold mx-auto mb-3;
}
/* ── FAQ accordion ── */
.faq-item {
@apply border-b border-parchment;
}
.faq-item summary {
@apply py-4 cursor-pointer font-medium text-espresso
list-none flex justify-between items-center;
}
.faq-item summary::after {
content: "+";
@apply text-copper text-xl font-light transition-transform;
}
.faq-item[open] summary::after {
content: "\2212";
}
.faq-item p {
@apply pb-4 text-sm text-stone leading-relaxed;
}
/* ── Chart container ── */
.chart-container {
@apply bg-white border border-parchment rounded-2xl p-4 md:p-6 shadow-sm;
}
.chart-container canvas {
width: 100% !important;
max-height: 400px;
}
/* ── Plan gate (upgrade prompt) ── */
.plan-gate {
@apply bg-latte border border-parchment rounded-xl p-4 text-sm text-stone text-center;
}
.plan-gate a {
@apply font-semibold;
}
/* ── Danger card ── */
.danger-card {
@apply bg-white border-2 border-danger/30 rounded-2xl p-6 shadow-sm;
}
/* ── Page header (dashboard) ── */
.page-header {
@apply mb-8;
}
.page-header h1 {
@apply text-3xl mb-1;
}
.page-header p {
@apply text-stone text-lg;
}
/* Cards */ /* Cards */
.card { .card {
@apply bg-white border border-parchment rounded-2xl p-6 mb-6 shadow-sm; @apply bg-white border border-parchment rounded-2xl p-6 mb-6 shadow-sm;

View File

@@ -5,6 +5,11 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% block title %}{{ config.APP_NAME }}{% endblock %}</title> <title>{% block title %}{{ config.APP_NAME }}{% endblock %}</title>
<!-- Fonts -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=DM+Sans:ital,opsz,wght@0,9..40,100..1000;1,9..40,100..1000&family=Fraunces:ital,opsz,wght@0,9..144,100..900;1,9..144,100..900&display=swap" rel="stylesheet">
<!-- Tailwind CSS --> <!-- Tailwind CSS -->
<link rel="stylesheet" href="{{ url_for('static', filename='css/output.css') }}"> <link rel="stylesheet" href="{{ url_for('static', filename='css/output.css') }}">
@@ -15,6 +20,13 @@
<nav class="nav-bar"> <nav class="nav-bar">
<div class="nav-inner"> <div class="nav-inner">
<a href="{{ url_for('public.landing') }}" class="nav-logo">{{ config.APP_NAME }}</a> <a href="{{ url_for('public.landing') }}" class="nav-logo">{{ config.APP_NAME }}</a>
<button class="nav-hamburger" aria-label="Toggle menu" onclick="document.querySelector('.nav-links').classList.toggle('open')">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round">
<line x1="3" y1="6" x2="21" y2="6"/>
<line x1="3" y1="12" x2="21" y2="12"/>
<line x1="3" y1="18" x2="21" y2="18"/>
</svg>
</button>
<div class="nav-links"> <div class="nav-links">
<a href="{{ url_for('public.features') }}">Features</a> <a href="{{ url_for('public.features') }}">Features</a>
<a href="{{ url_for('billing.pricing') }}">Pricing</a> <a href="{{ url_for('billing.pricing') }}">Pricing</a>
@@ -51,31 +63,33 @@
{% block content %}{% endblock %} {% block content %}{% endblock %}
<!-- Footer --> <!-- Footer -->
<footer class="container-page mt-16 py-8 border-t border-parchment"> <footer class="bg-latte mt-20">
<div class="grid-3"> <div class="container-page py-12">
<div> <div class="grid-3">
<strong class="text-espresso">{{ config.APP_NAME }}</strong> <div>
<p class="text-sm text-stone mt-1">Coffee market intelligence for independent traders.</p> <strong class="heading-display text-lg">{{ config.APP_NAME }}</strong>
<p class="text-sm text-stone mt-2 max-w-xs">Coffee commodity data for traders who refuse to overpay for market intelligence.</p>
</div>
<div>
<strong class="text-espresso text-sm">Product</strong>
<ul class="list-none p-0 mt-2 space-y-1.5 text-sm">
<li><a href="{{ url_for('public.features') }}">Features</a></li>
<li><a href="{{ url_for('billing.pricing') }}">Pricing</a></li>
<li><a href="{{ url_for('public.about') }}">About</a></li>
</ul>
</div>
<div>
<strong class="text-espresso text-sm">Legal</strong>
<ul class="list-none p-0 mt-2 space-y-1.5 text-sm">
<li><a href="{{ url_for('public.terms') }}">Terms</a></li>
<li><a href="{{ url_for('public.privacy') }}">Privacy</a></li>
</ul>
</div>
</div> </div>
<div> <div class="border-t border-parchment mt-8 pt-6 text-center text-xs text-stone">
<strong class="text-espresso text-sm">Product</strong> &copy; {{ now.year }} {{ config.APP_NAME }}. All rights reserved.
<ul class="list-none p-0 mt-1 space-y-1 text-sm">
<li><a href="{{ url_for('public.features') }}">Features</a></li>
<li><a href="{{ url_for('billing.pricing') }}">Pricing</a></li>
<li><a href="{{ url_for('public.about') }}">About</a></li>
</ul>
</div>
<div>
<strong class="text-espresso text-sm">Legal</strong>
<ul class="list-none p-0 mt-1 space-y-1 text-sm">
<li><a href="{{ url_for('public.terms') }}">Terms</a></li>
<li><a href="{{ url_for('public.privacy') }}">Privacy</a></li>
</ul>
</div> </div>
</div> </div>
<p class="text-center mt-8 text-xs text-stone">
&copy; {{ now.year }} {{ config.APP_NAME }}. All rights reserved.
</p>
</footer> </footer>
<!-- HTMX --> <!-- HTMX -->