Rewrite frontend templates: Pico CSS → Tailwind + trader-focused copy

Replace all Pico CSS patterns (classless articles, role="button", inline
styles, var(--pico-*)) with Tailwind component classes. Add Fraunces
display font, mobile hamburger nav, brand chart colors, and new component
layer (hero, feature-card, metric-card, auth-card, pricing-card, etc.).

Rewrite marketing copy from generic SaaS boilerplate to coffee-trader
focused messaging. Rebrand pricing tiers to Explorer/Trader/Analyst.
Delete stale custom.css. No Python code changes.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Deeman
2026-02-19 20:47:56 +01:00
parent fa6f3c70dd
commit f722854c07
16 changed files with 1407 additions and 839 deletions

View File

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

View File

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

View File

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

View File

@@ -1,119 +1,147 @@
{% extends "base.html" %}
{% block title %}Pricing - {{ config.APP_NAME }}{% endblock %}
{% block title %}Pricing {{ config.APP_NAME }}{% endblock %}
{% block content %}
<main class="container">
<header style="text-align: center; margin-bottom: 3rem;">
<h1>Simple, Transparent Pricing</h1>
<p>Start free with coffee data. Upgrade when you need more.</p>
</header>
<main>
<!-- Hero -->
<section class="hero pb-12">
<div class="container-page">
<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">
<!-- Free Plan -->
<article>
<header>
<h3>Free</h3>
<p><strong style="font-size: 2rem;">$0</strong> <small>/month</small></p>
</header>
<ul>
<li>Coffee dashboard</li>
<li>Last 5 years of data</li>
<li>Global &amp; country charts</li>
<li>Community support</li>
<!-- Pricing Cards -->
<section class="container-page pb-16">
<div class="grid-3 items-start">
<!-- Explorer (Free) -->
<div class="pricing-card">
<div class="mb-6">
<h3 class="text-xl mb-1">Explorer</h3>
<div class="flex items-baseline gap-1">
<span class="text-4xl font-bold text-espresso font-mono">$0</span>
<span class="text-stone text-sm">/forever</span>
</div>
</div>
<ul class="space-y-2 text-sm text-stone mb-8 flex-1">
<li>&#10003; Full coffee dashboard</li>
<li>&#10003; Last 5 years of data</li>
<li>&#10003; Global &amp; country charts</li>
<li>&#10003; Community support</li>
</ul>
<footer>
<div>
{% if user %}
{% if (user.plan or 'free') == 'free' %}
<button class="secondary" disabled>Current Plan</button>
<button class="btn-outline w-full" disabled>Current plan</button>
{% else %}
<button class="secondary" disabled>Free</button>
<button class="btn-outline w-full" disabled>Explorer</button>
{% endif %}
{% else %}
<a href="{{ url_for('auth.signup', plan='free') }}" role="button" class="secondary">Get Started</a>
<a href="{{ url_for('auth.signup', plan='free') }}" class="btn-outline w-full text-center">Start free</a>
{% endif %}
</footer>
</article>
</div>
</div>
<!-- Starter Plan -->
<article>
<header>
<h3>Starter</h3>
<p><strong style="font-size: 2rem;">TBD</strong> <small>/month</small></p>
</header>
<ul>
<li>Full coffee history (18+ years)</li>
<li>CSV data export</li>
<li>REST API access (10k calls/mo)</li>
<li>Email support</li>
<!-- Trader (Starter) — highlighted -->
<div class="pricing-card-highlighted">
<span class="pricing-badge">Most popular</span>
<div class="mb-6 pt-2">
<h3 class="text-xl mb-1">Trader</h3>
<div class="flex items-baseline gap-1">
<span class="text-4xl font-bold text-espresso font-mono">TBD</span>
<span class="text-stone text-sm">/mo</span>
</div>
</div>
<ul class="space-y-2 text-sm text-stone mb-8 flex-1">
<li>&#10003; Full coffee history (18+ years)</li>
<li>&#10003; CSV data export</li>
<li>&#10003; REST API access (10k calls/mo)</li>
<li>&#10003; Email support</li>
</ul>
<footer>
<div>
{% if user %}
{% if (user.plan or 'free') == 'starter' %}
<button class="secondary" disabled>Current Plan</button>
<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">Upgrade</button>
<button type="submit" class="btn w-full">Upgrade</button>
</form>
{% endif %}
{% else %}
<a href="{{ url_for('auth.signup', plan='starter') }}" role="button">Get Started</a>
<a href="{{ url_for('auth.signup', plan='starter') }}" class="btn w-full text-center">Get started</a>
{% endif %}
</footer>
</article>
</div>
</div>
<!-- Pro Plan -->
<article>
<header>
<h3>Pro</h3>
<p><strong style="font-size: 2rem;">TBD</strong> <small>/month</small></p>
</header>
<ul>
<li>All 65 USDA commodities</li>
<li>Unlimited API calls</li>
<li>CSV &amp; API export</li>
<li>Priority support</li>
<!-- Analyst (Pro) -->
<div class="pricing-card">
<div class="mb-6">
<h3 class="text-xl mb-1">Analyst</h3>
<div class="flex items-baseline gap-1">
<span class="text-4xl font-bold text-espresso font-mono">TBD</span>
<span class="text-stone text-sm">/mo</span>
</div>
</div>
<ul class="space-y-2 text-sm text-stone mb-8 flex-1">
<li>&#10003; All 65 USDA commodities</li>
<li>&#10003; Unlimited API calls</li>
<li>&#10003; CSV &amp; API export</li>
<li>&#10003; Priority support</li>
</ul>
<footer>
<div>
{% if user %}
{% if (user.plan or 'free') == 'pro' %}
<button class="secondary" disabled>Current Plan</button>
<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">Upgrade</button>
<button type="submit" class="btn-outline w-full">Upgrade</button>
</form>
{% endif %}
{% else %}
<a href="{{ url_for('auth.signup', plan='pro') }}" role="button">Get Started</a>
<a href="{{ url_for('auth.signup', plan='pro') }}" class="btn-outline w-full text-center">Get started</a>
{% endif %}
</footer>
</article>
</div>
</div>
</div>
</section>
<!-- FAQ -->
<section style="margin-top: 4rem; max-width: 600px; margin-left: auto; margin-right: auto;">
<h2>Frequently Asked Questions</h2>
<section class="container-page pb-16 max-w-2xl mx-auto">
<div class="section-heading">
<h2>Frequently asked questions</h2>
</div>
<details>
<details class="faq-item">
<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 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>
<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>
<summary>What commodities are available on Pro?</summary>
<p>All 65 commodities tracked by USDA PSD, including coffee, cocoa, sugar, cotton, grains, oilseeds, and more.</p>
<details class="faq-item">
<summary>What format is the CSV export?</summary>
<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>
<summary>How do I cancel?</summary>
<p>Cancel anytime from your dashboard settings. You keep access until the end of your billing period.</p>
<details class="faq-item">
<summary>How does the API work?</summary>
<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>
</section>
</main>

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,34 +1,48 @@
{% extends "base.html" %}
{% block title %}About - {{ config.APP_NAME }}{% endblock %}
{% block title %}About {{ config.APP_NAME }}{% endblock %}
{% block content %}
<main class="container">
<article style="max-width: 800px; margin: 0 auto;">
<header style="text-align: center;">
<h1>About {{ config.APP_NAME }}</h1>
</header>
<main>
<!-- Hero -->
<section class="hero">
<div class="container-page">
<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>
</div>
</section>
<section>
<p>{{ config.APP_NAME }} was built with a simple philosophy: ship fast, stay simple.</p>
<!-- Mission -->
<section class="container-page py-12 max-w-3xl mx-auto">
<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>
<p class="text-stone mb-8">None of these are built for you.</p>
<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>
<h2 class="text-2xl mb-4">What BeanFlows does</h2>
<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>
<p class="text-stone mb-8">No proprietary black box. The source data is public. Our methodology is transparent.</p>
<p>We took a different approach:</p>
<h2 class="text-2xl mb-4">How we build it</h2>
<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>
<ul>
<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>
<li><strong>Minimal dependencies</strong> Fewer things to break.</li>
<li><strong>Flat structure</strong> Find things where you expect them.</li>
<h2 class="text-2xl mb-4">Who this is for</h2>
<ul class="space-y-2 text-stone mb-8">
<li><strong class="text-espresso">Independent traders</strong> who need coffee supply &amp; demand data without a terminal subscription.</li>
<li><strong class="text-espresso">Roasters &amp; importers</strong> tracking origin country production trends for sourcing decisions.</li>
<li><strong class="text-espresso">Analysts &amp; researchers</strong> who want clean, exportable coffee commodity data.</li>
</ul>
<p>The result is a codebase you can understand in an afternoon and deploy for $5/month.</p>
<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>
<section style="text-align: center; margin-top: 3rem;">
<a href="{{ url_for('auth.signup') }}" role="button">Get Started</a>
<!-- 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>
</article>
</main>
{% endblock %}

View File

@@ -1,73 +1,129 @@
{% extends "base.html" %}
{% block title %}Features - {{ config.APP_NAME }}{% endblock %}
{% block title %}Features {{ config.APP_NAME }}{% endblock %}
{% block content %}
<main class="container">
<header style="text-align: center; margin-bottom: 3rem;">
<h1>Features</h1>
<p>Coffee market intelligence built on USDA Production, Supply &amp; Distribution data.</p>
</header>
<section>
<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>
<main>
<!-- Hero -->
<section class="hero">
<div class="container-page">
<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>
</div>
</section>
<section style="text-align: center; margin-top: 3rem;">
<a href="{{ url_for('auth.signup') }}" role="button">Start Free</a>
<a href="{{ url_for('billing.pricing') }}" role="button" class="secondary outline" style="margin-left: 1rem;">View Pricing</a>
<!-- Feature 1: Supply & Demand -->
<section class="container-page py-12">
<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>
</main>
{% endblock %}

View File

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

View File

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

View File

@@ -1,34 +1,35 @@
{% extends "base.html" %}
{% block title %}Terms of Service - {{ config.APP_NAME }}{% endblock %}
{% block title %}Terms of Service {{ config.APP_NAME }}{% endblock %}
{% block content %}
<main class="container">
<article style="max-width: 800px; margin: 0 auto;">
<header>
<main class="container-page py-12">
<div class="max-w-3xl mx-auto">
<div class="page-header">
<h1>Terms of Service</h1>
<p><small>Last updated: January 2024</small></p>
</header>
<p class="text-sm text-stone">Last updated: January 2024</p>
</div>
<section>
<h2>1. Acceptance of Terms</h2>
<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>
<div class="card">
<section class="mb-8">
<h2 class="text-xl mb-3">1. Acceptance of Terms</h2>
<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>
<p>{{ config.APP_NAME }} provides a software-as-a-service platform. Features and functionality may change over time.</p>
<section class="mb-8">
<h2 class="text-xl mb-3">2. Description of Service</h2>
<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>
<p>You are responsible for maintaining the security of your account. You must provide accurate information and keep it updated.</p>
<section class="mb-8">
<h2 class="text-xl mb-3">3. User Accounts</h2>
<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>
<p>You agree not to:</p>
<ul>
<section class="mb-8">
<h2 class="text-xl mb-3">4. Acceptable Use</h2>
<p class="text-stone mb-2">You agree not to:</p>
<ul class="list-disc pl-5 space-y-1 text-sm text-stone">
<li>Violate any laws or regulations</li>
<li>Infringe on intellectual property rights</li>
<li>Transmit harmful code or malware</li>
@@ -37,35 +38,36 @@
</ul>
</section>
<section>
<h2>5. Payment Terms</h2>
<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>
<section class="mb-8">
<h2 class="text-xl mb-3">5. Payment Terms</h2>
<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 class="mb-8">
<h2 class="text-xl mb-3">6. Termination</h2>
<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 class="mb-8">
<h2 class="text-xl mb-3">7. Disclaimer of Warranties</h2>
<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 class="mb-8">
<h2 class="text-xl mb-3">8. Limitation of Liability</h2>
<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 class="mb-8">
<h2 class="text-xl mb-3">9. Changes to Terms</h2>
<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>6. Termination</h2>
<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">10. Contact</h2>
<p class="text-stone">For questions about these terms, please contact us at {{ config.EMAIL_FROM }}.</p>
</section>
<section>
<h2>7. Disclaimer of Warranties</h2>
<p>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>
<p>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>
<p>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>
<p>For questions about these terms, please contact us at {{ config.EMAIL_FROM }}.</p>
</section>
</article>
</div>
</div>
</main>
{% 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

@@ -0,0 +1,442 @@
@import "tailwindcss";
/* ── BeanFlows Brand Theme ── */
@theme {
--font-display: "Fraunces", "DM Sans", ui-serif, Georgia, serif;
--font-sans: "DM Sans", ui-sans-serif, system-ui, -apple-system, sans-serif;
--font-mono: ui-monospace, "Cascadia Code", monospace;
--color-espresso: #2C1810;
--color-roast: #4A2C1A;
--color-copper: #B45309;
--color-copper-hover: #92400E;
--color-bean-green: #15803D;
--color-forest: #064E3B;
--color-cream: #FFFBF5;
--color-latte: #F5F0EB;
--color-parchment: #E8DFD5;
--color-stone: #78716C;
--color-stone-dark: #57534E;
--color-danger: #EF4444;
--color-danger-hover: #DC2626;
--color-warning: #D97706;
}
/* ── Base layer ── */
@layer base {
body {
@apply bg-cream text-stone-dark font-sans antialiased;
}
h1, h2, h3 {
font-family: var(--font-display);
@apply text-espresso font-bold tracking-tight;
}
h4, h5, h6 {
@apply text-roast font-semibold;
}
a {
@apply text-copper hover:text-copper-hover transition-colors;
}
hr {
@apply border-parchment my-6;
}
}
/* ── Component classes ── */
@layer components {
/* ── Navigation ── */
.nav-bar {
position: sticky;
top: 0;
z-index: 50;
background: rgba(255, 251, 245, 0.85);
backdrop-filter: blur(14px);
-webkit-backdrop-filter: blur(14px);
border-bottom: 1px solid rgba(232, 223, 213, 0.7);
}
.nav-inner {
max-width: 72rem;
margin: 0 auto;
padding: 0 1rem;
display: flex;
align-items: center;
justify-content: space-between;
height: 56px;
}
.nav-logo {
flex-shrink: 0;
font-family: var(--font-display);
font-weight: 700;
font-size: 1.125rem;
color: var(--color-espresso);
text-decoration: none;
}
.nav-logo:hover {
color: var(--color-espresso);
}
.nav-links {
display: flex;
align-items: center;
gap: 1.25rem;
font-size: 0.875rem;
font-weight: 500;
}
.nav-links a {
color: var(--color-stone-dark);
text-decoration: none;
transition: color 0.15s;
}
.nav-links a:hover {
color: var(--color-copper);
}
a.nav-auth-btn,
button.nav-auth-btn {
display: inline-flex;
align-items: center;
padding: 6px 16px;
border: none;
border-radius: 10px;
font-size: 0.8125rem;
font-weight: 600;
color: #fff;
background: var(--color-copper);
cursor: pointer;
text-decoration: none;
box-shadow: 0 2px 8px rgba(180, 83, 9, 0.25);
transition: background 0.15s;
}
a.nav-auth-btn:hover,
button.nav-auth-btn:hover {
background: var(--color-copper-hover);
color: #fff;
}
.nav-badge {
@apply bg-copper/10 text-copper px-2 py-0.5 text-xs font-semibold rounded-full;
}
.nav-form {
margin: 0;
padding: 0;
display: inline;
}
.nav-hamburger {
display: none;
background: none;
border: none;
cursor: pointer;
padding: 4px;
color: var(--color-espresso);
}
@media (max-width: 768px) {
.nav-hamburger { display: block; }
.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 */
.container-page {
@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 */
.card {
@apply bg-white border border-parchment rounded-2xl p-6 mb-6 shadow-sm;
}
.card-header {
@apply border-b border-parchment pb-3 mb-4 text-sm text-stone font-medium;
}
/* Buttons — shared base */
.btn, .btn-secondary, .btn-danger {
@apply inline-flex items-center justify-center px-5 py-2.5 rounded-xl
font-semibold text-sm transition-colors cursor-pointer
focus:outline-none focus:ring-2 focus:ring-copper/50;
}
.btn {
@apply bg-copper text-white hover:bg-copper-hover shadow-[0_2px_10px_rgba(180,83,9,0.25)];
}
.btn-secondary {
@apply bg-stone-dark text-white hover:bg-espresso;
}
.btn-danger {
@apply bg-danger text-white hover:bg-danger-hover;
}
.btn-outline {
@apply inline-flex items-center justify-center px-5 py-2.5 rounded-xl
font-semibold text-sm transition-colors cursor-pointer
bg-transparent text-stone-dark border border-parchment
hover:bg-latte hover:text-espresso
focus:outline-none focus:ring-2 focus:ring-copper/50;
}
.btn-sm {
@apply px-3 py-1.5 text-xs;
}
/* Forms */
.form-label {
@apply block text-sm font-medium text-roast mb-1;
}
.form-input {
@apply w-full px-3 py-2 rounded-xl border border-parchment bg-white
text-stone-dark placeholder-stone
focus:outline-none focus:ring-2 focus:ring-copper/50 focus:border-copper
transition-colors;
}
.form-hint {
@apply text-xs text-stone mt-1;
}
/* Tables */
.table {
@apply w-full text-sm;
}
.table th {
@apply text-left px-3 py-2 text-xs font-semibold text-stone uppercase tracking-wider
border-b-2 border-parchment;
}
.table td {
@apply px-3 py-2 border-b border-parchment text-stone-dark;
}
.table tbody tr:hover {
@apply bg-latte;
}
/* Flash messages */
.flash, .flash-error, .flash-success, .flash-warning {
@apply px-4 py-3 rounded-xl mb-4 border-l-4 bg-white text-stone-dark text-sm;
}
.flash {
@apply border-copper;
}
.flash-error {
@apply border-danger;
}
.flash-success {
@apply border-bean-green;
}
.flash-warning {
@apply border-warning;
}
/* Badges */
.badge, .badge-success, .badge-danger, .badge-warning {
@apply inline-block px-2 py-0.5 text-xs font-semibold rounded-full;
}
.badge {
@apply bg-copper/10 text-copper;
}
.badge-success {
@apply bg-bean-green/10 text-bean-green;
}
.badge-danger {
@apply bg-danger/10 text-danger;
}
.badge-warning {
@apply bg-warning/10 text-warning;
}
/* Heading group */
.heading-group {
@apply mb-6;
}
.heading-group h1,
.heading-group h2 {
@apply mb-1;
}
.heading-group p {
@apply text-stone text-lg;
}
/* Grid helpers */
.grid-auto {
@apply grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6;
}
.grid-2 {
@apply grid grid-cols-1 md:grid-cols-2 gap-6;
}
.grid-3 {
@apply grid grid-cols-1 md:grid-cols-3 gap-6;
}
.grid-4 {
@apply grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-6;
}
/* Monospace data display */
.metric {
@apply font-mono text-espresso;
}
.mono {
@apply font-mono;
}
/* HTMX loading indicators */
.htmx-indicator {
display: none;
}
.htmx-request .htmx-indicator,
.htmx-request.htmx-indicator {
display: inline;
}
/* Code blocks */
code {
@apply font-mono text-sm bg-latte px-1 py-0.5 rounded;
}
}

View File

@@ -1,59 +1,60 @@
<!DOCTYPE html>
<html lang="en" data-theme="light">
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% block title %}{{ config.APP_NAME }}{% endblock %}</title>
<!-- Pico CSS -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@picocss/pico@2/css/pico.min.css">
<!-- 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">
<!-- Custom styles -->
<link rel="stylesheet" href="{{ url_for('static', filename='css/custom.css') }}">
<!-- Tailwind CSS -->
<link rel="stylesheet" href="{{ url_for('static', filename='css/output.css') }}">
{% block head %}{% endblock %}
</head>
<body>
<!-- Navigation -->
<nav class="container">
<ul>
<li><a href="{{ url_for('public.landing') }}"><strong>{{ config.APP_NAME }}</strong></a></li>
</ul>
<ul>
<li><a href="{{ url_for('public.features') }}">Features</a></li>
<li><a href="{{ url_for('billing.pricing') }}">Pricing</a></li>
<nav class="nav-bar">
<div class="nav-inner">
<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">
<a href="{{ url_for('public.features') }}">Features</a>
<a href="{{ url_for('billing.pricing') }}">Pricing</a>
{% if user %}
<li><a href="{{ url_for('dashboard.index') }}">Dashboard</a></li>
<a href="{{ url_for('dashboard.index') }}">Dashboard</a>
{% if session.get('is_admin') %}
<li><a href="{{ url_for('admin.index') }}"><mark>Admin</mark></a></li>
<a href="{{ url_for('admin.index') }}"><span class="nav-badge">Admin</span></a>
{% endif %}
<li>
<form method="post" action="{{ url_for('auth.logout') }}" style="margin: 0;">
<form method="post" action="{{ url_for('auth.logout') }}" class="nav-form">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<button type="submit" class="outline secondary" style="padding: 0.5rem 1rem; margin: 0;">Sign Out</button>
<button type="submit" class="btn-outline btn-sm">Sign Out</button>
</form>
</li>
{% else %}
<li><a href="{{ url_for('auth.login') }}">Sign In</a></li>
<li><a href="{{ url_for('auth.signup') }}" role="button">Get Started</a></li>
<a href="{{ url_for('auth.login') }}">Sign In</a>
<a href="{{ url_for('auth.signup') }}" class="nav-auth-btn">Get Started</a>
{% endif %}
</ul>
</div>
</div>
</nav>
<!-- Flash messages -->
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
<div class="container">
<div class="container-page mt-4">
{% for category, message in messages %}
<article
style="padding: 1rem; margin-bottom: 1rem;
{% if category == 'error' %}border-left: 4px solid var(--del-color);
{% elif category == 'success' %}border-left: 4px solid var(--ins-color);
{% elif category == 'warning' %}border-left: 4px solid var(--mark-background-color);
{% else %}border-left: 4px solid var(--primary);{% endif %}"
>
<div class="{% if category == 'error' %}flash-error{% elif category == 'success' %}flash-success{% elif category == 'warning' %}flash-warning{% else %}flash{% endif %}">
{{ message }}
</article>
</div>
{% endfor %}
</div>
{% endif %}
@@ -62,34 +63,36 @@
{% block content %}{% endblock %}
<!-- Footer -->
<footer class="container" style="margin-top: 4rem; padding: 2rem 0; border-top: 1px solid var(--muted-border-color);">
<div class="grid">
<footer class="bg-latte mt-20">
<div class="container-page py-12">
<div class="grid-3">
<div>
<strong>{{ config.APP_NAME }}</strong>
<p><small>Coffee market intelligence for independent traders.</small></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>Product</strong>
<ul style="list-style: none; padding: 0;">
<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>Legal</strong>
<ul style="list-style: none; padding: 0;">
<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>
<p style="text-align: center; margin-top: 2rem;">
<small>&copy; {{ now.year }} {{ config.APP_NAME }}. All rights reserved.</small>
</p>
<div class="border-t border-parchment mt-8 pt-6 text-center text-xs text-stone">
&copy; {{ now.year }} {{ config.APP_NAME }}. All rights reserved.
</div>
</div>
</footer>
<!-- HTMX (optional) -->
<!-- HTMX -->
<script src="https://unpkg.com/htmx.org@2.0.4"></script>
{% block scripts %}{% endblock %}