Files
data-entry-app/frontend/src/routes/+page.svelte
T
2026-06-11 23:56:02 +12:00

2494 lines
57 KiB
Svelte

<script lang="ts">
import { api } from '$lib/api';
import { goto } from '$app/navigation';
import { clientSession, sessionHydrated } from '$lib/session';
import Skeleton from '$lib/components/Skeleton.svelte';
import type { DashboardSummary } from '$lib/types';
import { canOpenEditor, getWorkspaceHomeHref } from '$lib/workspace-access';
import packageInfo from '../../package.json';
import {
ArrowUpRight,
BadgeDollarSign,
Factory,
PackageCheck,
Scale,
Sun,
Sunrise,
Sunset,
TriangleAlert,
Moon
} from 'lucide-svelte';
import { tick } from 'svelte';
type Segment = {
label: string;
value: number;
color: string;
};
type GaugeBar = {
x1: number;
y1: number;
x2: number;
y2: number;
color: string;
};
type WorkspaceFocus = {
code: string;
label: string;
detail: string;
value: string;
tone: 'positive' | 'warning' | 'neutral';
};
let { data } = $props();
let email = $state('');
let password = $state('');
let isLoggingIn = $state(false);
let postLoginRedirecting = $state(false);
let loginError = $state('');
let emailInput = $state<HTMLInputElement | null>(null);
let passwordInput = $state<HTMLInputElement | null>(null);
let loginFocusArmed = $state(true);
const currentYear = new Date().getFullYear();
const appVersion = `v${packageInfo.version}`;
const monthLabels = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep'];
async function handleLogin(event: SubmitEvent) {
event.preventDefault();
loginError = '';
isLoggingIn = true;
try {
// Authenticates against the internal Hunter Stock Feeds role/permission
// system first. If that fails (e.g. a B2B ordering-portal customer, who
// lives in the ClientUser table and signs in with the shared client
// password), fall back to the client login. Both responses are
// shape-compatible with the client session.
let session;
try {
session = await api.internalLogin(email, password);
} catch {
session = await api.clientLogin(email, password);
}
const targetHref = getWorkspaceHomeHref(session);
postLoginRedirecting = targetHref !== '/';
clientSession.set(session);
if (targetHref !== '/') {
await goto(targetHref, { replaceState: true });
}
} catch (error) {
loginError = error instanceof Error ? error.message : 'Unable to sign in';
triggerPasswordShake();
} finally {
postLoginRedirecting = false;
isLoggingIn = false;
}
}
function triggerPasswordShake() {
if (typeof window === 'undefined' || !passwordInput) {
return;
}
if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) {
return;
}
passwordInput.animate(
[
{ transform: 'translateX(0)' },
{ transform: 'translateX(-8px)' },
{ transform: 'translateX(8px)' },
{ transform: 'translateX(-6px)' },
{ transform: 'translateX(6px)' },
{ transform: 'translateX(0)' }
],
{
duration: 360,
easing: 'cubic-bezier(0.36, 0.07, 0.19, 0.97)'
}
);
}
$effect(() => {
if ($sessionHydrated && !$clientSession) {
if (loginFocusArmed && emailInput) {
loginFocusArmed = false;
tick().then(() => emailInput?.focus());
}
return;
}
loginFocusArmed = true;
});
function currency(value: number | null | undefined, digits = 2) {
if (value === null || value === undefined) {
return 'N/A';
}
return `$${value.toFixed(digits)}`;
}
function kg(value: number | null | undefined) {
if (value === null || value === undefined) {
return '0 kg';
}
return `${value.toLocaleString(undefined, { maximumFractionDigits: 0 })} kg`;
}
function formatDate(value: string | null | undefined) {
if (!value) {
return 'No date';
}
return new Intl.DateTimeFormat('en-NZ', {
day: 'numeric',
month: 'short',
year: 'numeric'
}).format(new Date(value));
}
// Australian Eastern time-of-day → greeting + matching Lucide icon.
function timeOfDay() {
const astHour = Number(
new Intl.DateTimeFormat('en-AU', {
hour: 'numeric',
hour12: false,
timeZone: 'Australia/Brisbane'
}).format(new Date())
);
if (astHour >= 5 && astHour < 12) return { label: 'Good morning', icon: Sunrise, tone: 'morning' as const };
if (astHour >= 12 && astHour < 17) return { label: 'Good afternoon', icon: Sun, tone: 'afternoon' as const };
if (astHour >= 17 && astHour < 21) return { label: 'Good evening', icon: Sunset, tone: 'evening' as const };
return { label: 'Good evening', icon: Moon, tone: 'night' as const };
}
function firstName(name: string | null | undefined) {
return name?.trim().split(/\s+/)[0] ?? 'there';
}
// The dashboard summary streams in after the route shell paints. Until it
// resolves, all derived state falls back to defaults so the page chrome
// stays interactive.
let summary = $state<DashboardSummary | null>(null);
$effect(() => {
let cancelled = false;
Promise.resolve(data.summary).then((value) => {
if (!cancelled) summary = value;
});
return () => {
cancelled = true;
};
});
function buildSegments(current: DashboardSummary | null) {
return [
{ label: 'Materials', value: current?.raw_materials?.count ?? 0, color: '#15803d' },
{ label: 'Mixes', value: current?.mixes?.count ?? 0, color: '#bf8700' },
{ label: 'Products', value: current?.products?.count ?? 0, color: '#0969da' }
];
}
function polar(cx: number, cy: number, radius: number, angle: number) {
const radians = ((angle - 90) * Math.PI) / 180;
return {
x: cx + radius * Math.cos(radians),
y: cy + radius * Math.sin(radians)
};
}
function buildGaugeBars(segments: Segment[]) {
const total = segments.reduce((sum, segment) => sum + segment.value, 0) || 1;
const stops = segments.reduce<Array<{ threshold: number; color: string }>>((list, segment, index) => {
const previous = list[index - 1]?.threshold ?? 0;
list.push({
threshold: previous + segment.value / total,
color: segment.color
});
return list;
}, []);
return Array.from({ length: 24 }, (_, index) => {
const ratio = (index + 1) / 24;
const activeStop = stops.find((stop) => ratio <= stop.threshold) ?? stops[stops.length - 1];
const angle = -112 + index * (224 / 23);
const inner = polar(120, 126, 58, angle);
const outer = polar(120, 126, 95, angle);
return {
x1: Number(inner.x.toFixed(2)),
y1: Number(inner.y.toFixed(2)),
x2: Number(outer.x.toFixed(2)),
y2: Number(outer.y.toFixed(2)),
color: activeStop?.color ?? '#15803d'
};
});
}
function buildTrendSeries(current: DashboardSummary | null) {
const trends = current?.trend_seeds;
const seeds = [
...(trends?.raw_material_cost_per_kg ?? []).map((value) => value * 780),
...(trends?.mix_cost_per_kg ?? []).map((value) => value * 640),
...(trends?.product_finished_delivered ?? []).map((value) => value * 24)
].filter((value) => value > 0);
const source = seeds.length ? seeds : [320, 360, 420];
return monthLabels.map((_, index) => {
const base = source[index % source.length];
const swing = [0.86, 1.03, 0.78, 0.96, 1.18, 1.02, 1.12, 0.91, 1.14][index];
return Math.round(base * swing);
});
}
function chartPoints(values: number[]) {
const min = Math.min(...values);
const max = Math.max(...values);
return values.map((value, index) => {
const x = values.length === 1 ? 50 : (index * 100) / (values.length - 1);
const y = max === min ? 28 : 6 + ((max - value) / (max - min)) * 38;
return {
x: Number(x.toFixed(2)),
y: Number(y.toFixed(2)),
value
};
});
}
function linePath(values: number[]) {
return chartPoints(values)
.map((point, index) => `${index === 0 ? 'M' : 'L'} ${point.x} ${point.y}`)
.join(' ');
}
function areaPath(values: number[]) {
const points = chartPoints(values);
const head = points.map((point, index) => `${index === 0 ? 'M' : 'L'} ${point.x} ${point.y}`).join(' ');
return `${head} L 100 52 L 0 52 Z`;
}
function focusMarker(values: number[]) {
const points = chartPoints(values);
const peak = points.reduce((best, point, index) => {
if (point.value > best.value) {
return { ...point, index };
}
return best;
}, { ...points[0], index: 0 });
return {
left: `${peak.x}%`,
label: `${peak.value}`,
month: monthLabels[peak.index] ?? monthLabels[0]
};
}
function buildFocusCards(current: DashboardSummary | null): WorkspaceFocus[] {
const featuredMaterial = current?.raw_materials?.latest ?? null;
const featuredMix = current?.mixes?.top ?? null;
const featuredProduct = current?.products?.top ?? null;
return [
{
code: 'RM',
label: featuredMaterial?.name ?? 'Raw material',
detail: `Updated ${formatDate(featuredMaterial?.effective_date)}`,
value: currency(featuredMaterial?.market_value),
tone: 'positive'
},
{
code: 'MX',
label: featuredMix?.name ?? 'Mix worksheet',
detail: `${featuredMix?.ingredients_count ?? 0} ingredients loaded`,
value: `${currency(featuredMix?.mix_cost_per_kg, 4)} / kg`,
tone: featuredMix?.warnings.length ? 'warning' : 'neutral'
},
{
code: 'PR',
label: featuredProduct?.product_name ?? 'Delivered product',
detail: featuredProduct?.warnings.length ? 'Warnings need review' : 'Pricing output is stable',
value: currency(featuredProduct?.finished_product_delivered),
tone: featuredProduct?.warnings.length ? 'warning' : 'positive'
}
];
}
const featuredProduct = $derived(summary?.products?.top ?? null);
const featuredMix = $derived(summary?.mixes?.top ?? null);
const featuredMaterial = $derived(summary?.raw_materials?.latest ?? null);
const productionSegments = $derived(buildSegments(summary));
const gaugeBars = $derived(buildGaugeBars(productionSegments));
const totalTracked = $derived(
productionSegments.reduce((sum: number, segment: Segment) => sum + segment.value, 0)
);
const totalMarketValue = $derived(summary?.raw_materials?.total_market_value ?? 0);
const averageMixCost = $derived(summary?.mixes?.average_cost_per_kg ?? 0);
const trendSeries = $derived(buildTrendSeries(summary));
const trendLine = $derived(linePath(trendSeries));
const trendArea = $derived(areaPath(trendSeries));
const trendFocus = $derived(focusMarker(trendSeries));
const topProducts = $derived(summary?.products?.top_products ?? []);
const operations = $derived(summary?.operations ?? null);
const focusCards = $derived(buildFocusCards(summary));
const loading = $derived(summary === null);
const greeting = $derived(timeOfDay());
</script>
{#if !$sessionHydrated}
<section class="auth-stage auth-stage-loading">
<div class="auth-card auth-card-loading">
<div class="auth-header">
<div class="client-logo-block">
<img class="hero-login-logo" src="/logo-hsf.png" alt="Lean 101" />
<div class="client-logo-copy">
<p class="eyebrow">Client Workspace</p>
<strong>Hunter Premium Produce</strong>
<span>Lean 101 client workspace access</span>
</div>
</div>
</div>
<div class="auth-copy">
<h2>Restoring your workspace.</h2>
<p>Checking the saved client session before deciding whether sign-in is required.</p>
</div>
<div class="auth-loading-panel">
<span class="loading-pulse" aria-hidden="true"></span>
<div>
<strong>Checking Session</strong>
<p>The sign-in form appears only when no valid local client session is available.</p>
</div>
</div>
<div class="auth-footer">
<div class="lean-brand">
<img class="lean-isotipo" src="/lean101-isotipo.png" alt="Lean 101" />
<span class="powered-by-label">Powered by Lean 101</span>
</div>
<div class="auth-meta">
<span class="version-badge">{appVersion}</span>
<span>&copy; {currentYear} Hunter Premium Produce</span>
</div>
</div>
</div>
</section>
{:else if !$clientSession}
<section class="auth-stage">
<div class="auth-card auth-card-login">
<div class="auth-header">
<div class="client-logo-block">
<img class="hero-login-logo" src="/logo-hsf.png" alt="Lean 101" />
<div class="client-logo-copy">
<p class="eyebrow">Client Sign-In</p>
<strong>Hunter Premium Produce</strong>
</div>
</div>
<div class="auth-status-row">
<span class="auth-status-pill">Secure Workspace Access</span>
</div>
</div>
<div class="auth-copy">
<h2>Welcome back</h2>
<p>Sign in with your email and password to continue.</p>
</div>
<form class="signin-form auth-form" onsubmit={handleLogin}>
<label class="field">
<span>Email</span>
<input
bind:this={emailInput}
bind:value={email}
type="email"
autocomplete="username"
placeholder="Email"
/>
</label>
<label class="field field-password" class:is-invalid={Boolean(loginError)}>
<span>Password</span>
<input
bind:this={passwordInput}
bind:value={password}
type="password"
autocomplete="current-password"
placeholder="Password"
/>
</label>
<button class="primary-button auth-submit" type="submit" disabled={isLoggingIn}>
{isLoggingIn ? 'Signing in...' : 'Sign In'}
</button>
</form>
{#if loginError}
<p class="login-error">{loginError}</p>
{/if}
<div class="auth-footer">
<div class="lean-brand">
<img class="lean-isotipo" src="/lean101-isotipo.png" alt="Lean 101" />
<span class="powered-by-label">Powered by Lean 101</span>
</div>
<div class="auth-meta">
<span class="version-badge">{appVersion}</span>
<span>&copy; {currentYear} Lean 101</span>
</div>
</div>
</div>
</section>
{:else if postLoginRedirecting}
<section class="auth-stage auth-stage-loading">
<div class="auth-card auth-card-loading">
<div class="auth-header">
<div class="client-logo-block">
<img class="hero-login-logo" src="/logo-hsf.png" alt="Lean 101" />
<div class="client-logo-copy">
<p class="eyebrow">Opening Workspace</p>
<strong>Hunter Premium Produce</strong>
<span>Applying your role permissions now</span>
</div>
</div>
</div>
<div class="auth-copy">
<h2>Preparing your workspace.</h2>
<p>Routing you directly to the first area your role is allowed to open.</p>
</div>
<div class="auth-loading-panel">
<span class="loading-pulse" aria-hidden="true"></span>
<div>
<strong>Applying Access Rules</strong>
<p>Dashboard access is skipped for roles that do not have permission.</p>
</div>
</div>
</div>
</section>
{:else}
<section class="dashboard-intro">
<div class="greeting-row">
{#snippet greetIcon()}
{@const Icon = greeting.icon}
<span class={`greeting-icon ${greeting.tone}`} aria-hidden="true">
<Icon size={44} strokeWidth={1.6} />
</span>
{/snippet}
{@render greetIcon()}
<h2>{greeting.label}, {firstName($clientSession?.name)}</h2>
</div>
<div class="intro-actions">
{#if canOpenEditor($clientSession)}
<a class="primary-button" href="/editor">Open Mix Editor</a>
{/if}
</div>
</section>
<section class="focus-row">
{#each focusCards as card, i}
<article class={`focus-card ${card.tone}`}>
<span class="focus-code">{card.code}</span>
<div>
{#if loading}
<Skeleton width="9rem" height="0.95rem" />
<Skeleton width="6rem" height="0.7rem" />
{:else}
<strong>{card.label}</strong>
<span>{card.detail}</span>
{/if}
</div>
{#if loading}
<Skeleton width="4rem" height="1rem" />
{:else}
<em>{card.value}</em>
{/if}
</article>
{/each}
</section>
<section class="dashboard-grid">
<article class="panel-card market-card">
<div class="card-toolbar">
<span class="pill success">Latest market check</span>
<div class="toggle-pill">
<span class="active">NZD</span>
<span>USD</span>
</div>
</div>
<div class="market-layout">
<div>
{#if loading}
<Skeleton width="14rem" height="1.5rem" />
<div style="height:0.5rem"></div>
<Skeleton width="8rem" height="0.85rem" />
<div class="hero-value"><Skeleton width="9rem" height="2.6rem" /></div>
<Skeleton width="11rem" height="0.85rem" />
{:else}
<h3>{featuredMaterial?.name ?? 'No material loaded'}</h3>
<p>{formatDate(featuredMaterial?.effective_date)}</p>
<div class="hero-value">{currency(featuredMaterial?.market_value)}</div>
<p class="support-text">
{currency(featuredMaterial?.cost_per_kg, 4)} / kg
<span>Current blend for Hunter Premium Produce</span>
</p>
{/if}
</div>
<div class="field-emblem" aria-hidden="true">
<span class="sun-core"></span>
<span class="field-stripe one"></span>
<span class="field-stripe two"></span>
<span class="field-stripe three"></span>
</div>
</div>
</article>
<article class="panel-card gauge-card">
<div class="card-toolbar">
<div>
<h3>Tracked Workspace</h3>
<p>Entities currently feeding the Hunter costing model</p>
</div>
<button class="secondary-button compact" type="button">Snapshot</button>
</div>
<div class="gauge-visual">
<svg viewBox="0 0 240 150" aria-hidden="true">
{#each gaugeBars as bar}
<line
x1={bar.x1}
y1={bar.y1}
x2={bar.x2}
y2={bar.y2}
stroke={bar.color}
stroke-width="11"
stroke-linecap="round"
/>
{/each}
</svg>
<div class="gauge-center">
<strong>{totalTracked}</strong>
<span>tracked items</span>
</div>
</div>
<div class="legend-row">
{#each productionSegments as segment}
<span><i style={`background:${segment.color};`}></i>{segment.label} {segment.value}</span>
{/each}
</div>
</article>
<div class="metric-stack">
<article class="panel-card metric-card">
<div class="metric-head">
<span>Total Input Spend</span>
<span class="metric-icon"></span>
</div>
{#if loading}
<strong><Skeleton width="6rem" height="1.6rem" /></strong>
{:else}
<strong>{currency(totalMarketValue)}</strong>
{/if}
<p>Across all tracked raw materials</p>
</article>
<article class="panel-card metric-card">
<div class="metric-head">
<span>Average Mix Cost</span>
<span class="metric-icon"></span>
</div>
{#if loading}
<strong><Skeleton width="6rem" height="1.6rem" /></strong>
{:else}
<strong>{currency(averageMixCost, 4)}</strong>
{/if}
<p>Per kg across the current mix set</p>
</article>
<article class="panel-card metric-card">
<div class="metric-head">
<span>Top Delivered Output</span>
<span class="metric-icon"></span>
</div>
{#if loading}
<strong><Skeleton width="6rem" height="1.6rem" /></strong>
<p><Skeleton width="9rem" height="0.85rem" /></p>
{:else}
<strong>{currency(featuredProduct?.finished_product_delivered)}</strong>
<p>{featuredProduct?.product_name ?? 'No products loaded'}</p>
{/if}
</article>
</div>
</section>
<section class="operations-report">
<div class="card-toolbar operations-toolbar">
<div class="operations-heading">
<span class="operations-icon"><Factory size={24} strokeWidth={2.2} /></span>
<div>
<h3>Production And Pricing</h3>
<p>Throughput and Product Costing for {operations?.period_label ?? 'this month'}</p>
</div>
</div>
<a class="secondary-button compact operations-link" href="/product-costing">
Open Product Costing
<ArrowUpRight size={15} strokeWidth={2.3} />
</a>
</div>
<div class="operations-graphic" aria-hidden="true">
<div class="operations-graphic-labels">
<span>Throughput</span>
<span>Costing</span>
<span>Pricing</span>
</div>
<div class="operations-graphic-track">
<span class="operation-node production-node"></span>
<span class="operation-track"></span>
<span class="operation-bars">
<i></i>
<i></i>
<i></i>
</span>
<span class="operation-node pricing-node"></span>
</div>
</div>
<div class="operations-metrics">
<article class="produced">
<div class="metric-label">
<span>Produced</span>
<span class="metric-symbol"><PackageCheck size={17} strokeWidth={2.2} /></span>
</div>
{#if loading}
<strong><Skeleton width="6rem" height="1.5rem" /></strong>
{:else}
<strong>{kg(operations?.total_kg)}</strong>
{/if}
<p>{operations?.entry_count ?? 0} throughput entries</p>
</article>
<article class="bags">
<div class="metric-label">
<span>Bags</span>
<span class="metric-symbol"><Scale size={17} strokeWidth={2.2} /></span>
</div>
{#if loading}
<strong><Skeleton width="5rem" height="1.5rem" /></strong>
{:else}
<strong>{(operations?.total_bags ?? 0).toLocaleString(undefined, { maximumFractionDigits: 0 })}</strong>
{/if}
<p>Logged as bag runs</p>
</article>
<article class="value">
<div class="metric-label">
<span>Wholesale Value</span>
<span class="metric-symbol"><BadgeDollarSign size={17} strokeWidth={2.2} /></span>
</div>
{#if loading}
<strong><Skeleton width="7rem" height="1.5rem" /></strong>
{:else}
<strong>{currency(operations?.estimated_wholesale_value)}</strong>
{/if}
<p>{operations?.priced_entry_count ?? 0} priced entries</p>
</article>
<article class:warning={(operations?.pricing_issues?.total ?? 0) > 0}>
<div class="metric-label">
<span>Pricing Issues</span>
<span class="metric-symbol"><TriangleAlert size={17} strokeWidth={2.2} /></span>
</div>
{#if loading}
<strong><Skeleton width="4rem" height="1.5rem" /></strong>
{:else}
<strong>{operations?.pricing_issues?.total ?? 0}</strong>
{/if}
<p>Products needing review</p>
</article>
</div>
<div class="operations-grid">
<article>
<div class="mini-heading">
<strong>Top Produced</strong>
<span>By kg</span>
</div>
<div class="report-list">
{#if loading}
{#each Array(4) as _}
<div><Skeleton width="10rem" /><Skeleton width="4rem" /></div>
{/each}
{:else if operations?.top_products?.length}
{#each operations.top_products as product}
<div>
<span>
<strong>{product.product_name}</strong>
<small>{product.client_name ?? 'No client'} · {product.entries} entries</small>
</span>
<em>{kg(product.kg)}</em>
</div>
{/each}
{:else}
<p>No throughput recorded this month.</p>
{/if}
</div>
</article>
<article>
<div class="mini-heading">
<strong>Produced But Not Priced</strong>
<span>Fix these first</span>
</div>
<div class="report-list">
{#if loading}
{#each Array(4) as _}
<div><Skeleton width="10rem" /><Skeleton width="4rem" /></div>
{/each}
{:else if operations?.produced_not_priced?.length}
{#each operations.produced_not_priced as product}
<div>
<span>
<strong>{product.product_name}</strong>
<small>{product.warnings[0] ?? product.status}</small>
</span>
<em>{kg(product.kg)}</em>
</div>
{/each}
{:else}
<p>All produced products have usable pricing.</p>
{/if}
</div>
</article>
</div>
</section>
<section class="analysis-grid">
<article class="panel-card chart-card">
<div class="card-toolbar">
<div>
<h3>Monthly Pricing Pulse</h3>
<p>Trend view of input pressure and delivered output movement</p>
</div>
<div class="chart-actions">
<button class="secondary-button compact" type="button">Mixes</button>
<button class="secondary-button compact" type="button">2026</button>
</div>
</div>
<div class="chart-shell">
<div class="focus-badge" style={`left:${trendFocus.left};`}>
<span>Peak {trendFocus.month}</span>
<strong>{trendFocus.label}</strong>
</div>
<svg viewBox="0 0 100 56" preserveAspectRatio="none" aria-hidden="true">
<defs>
<linearGradient id="chart-fill" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stop-color="#15803d" stop-opacity="0.22" />
<stop offset="100%" stop-color="#15803d" stop-opacity="0.02" />
</linearGradient>
</defs>
<path d={trendArea} fill="url(#chart-fill)"></path>
<path d={trendLine} fill="none" stroke="#15803d" stroke-width="1.6" stroke-linecap="round"></path>
</svg>
</div>
<div class="month-row">
{#each monthLabels as label}
<span>{label}</span>
{/each}
</div>
</article>
<article class="panel-card preview-card">
<div class="orchard-visual">
<div class="orchard-sky"></div>
<div class="orchard-sun"></div>
<div class="orchard-hill"></div>
<div class="orchard-row left"></div>
<div class="orchard-row center"></div>
<div class="orchard-row right"></div>
</div>
<div class="preview-body">
<div class="preview-header">
<div>
<h3>{featuredMix?.name ?? 'Mix Preview'}</h3>
<p>{featuredMix?.client_name ?? 'Hunter Premium Produce'}</p>
</div>
<a href="/mixes">Open Mix Master</a>
</div>
<div class="preview-facts">
<article>
<span>Ingredients</span>
<strong>{featuredMix?.ingredients_count ?? 0}</strong>
</article>
<article>
<span>Total Kg</span>
<strong>{featuredMix?.total_mix_kg ?? 0}</strong>
</article>
<article>
<span>Total Cost</span>
<strong>{currency(featuredMix?.total_mix_cost)}</strong>
</article>
</div>
</div>
</article>
</section>
<section class="detail-grid">
<article class="panel-card task-card">
<div class="card-toolbar">
<div>
<h3>Priority Watchlist</h3>
<p>Current client-facing checkpoints generated from the active costing snapshot</p>
</div>
</div>
<div class="table-wrap">
<table>
<thead>
<tr>
<th>Focus</th>
<th>Owner</th>
<th>Reference Date</th>
<th>Status</th>
</tr>
</thead>
<tbody>
{#each focusCards as card}
<tr>
<td class="task-cell" data-label="Focus">
<div class="table-item">
<span class={`task-icon ${card.tone}`}>{card.code}</span>
<div>
<strong>{card.label}</strong>
<span>{card.value}</span>
</div>
</div>
</td>
<td data-label="Owner">
<div class="owner-chip">
<span>HP</span>
<strong>Hunter Premium Produce</strong>
</div>
</td>
<td data-label="Reference Date">
<div class="due-block">
<strong>{card.detail}</strong>
<span>Current checkpoint</span>
</div>
</td>
<td data-label="Status">
<span class={`status-chip ${card.tone}`}>{card.tone === 'warning' ? 'Watch' : 'On track'}</span>
</td>
</tr>
{/each}
</tbody>
</table>
</div>
</article>
<article class="panel-card summary-card">
<div class="card-toolbar">
<div>
<h3>Finished Product Summary</h3>
<p>Highest delivered pricing outputs</p>
</div>
</div>
<div class="summary-list">
{#each topProducts as product}
<article>
<div class="summary-name">
<span class="summary-dot"></span>
<div>
<strong>{product.product_name}</strong>
<span>{product.warnings.length ? 'Review warnings' : 'Stable pricing output'}</span>
</div>
</div>
<strong>{currency(product.finished_product_delivered)}</strong>
</article>
{/each}
</div>
</article>
</section>
{/if}
<style>
h2,
h3,
p {
margin: 0;
}
.eyebrow {
color: #85958c;
font-size: 0.78rem;
font-weight: 600;
letter-spacing: 0.08em;
text-transform: uppercase;
}
.auth-stage {
min-height: calc(100vh - 3rem);
display: grid;
place-items: center;
padding: 1rem 0;
}
.auth-stage-loading {
align-items: center;
}
.auth-card {
position: relative;
width: min(100%, 38rem);
display: grid;
gap: 1.35rem;
padding: 2.1rem 2rem 1.6rem;
border: 1px solid var(--color-border);
border-radius: 1.25rem;
background: var(--color-bg-surface);
overflow: hidden;
}
/* Crisp brand accent along the top edge, clipped to the card radius.
Replaces the old green radial glow, which read as a muddy shadow. */
.auth-card::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
height: 4px;
background: var(--color-brand);
pointer-events: none;
}
.auth-card > * {
position: relative;
z-index: 1;
}
.auth-card-loading {
width: min(100%, 34rem);
}
.auth-card-login {
width: min(100%, 39rem);
animation: login-card-enter 420ms cubic-bezier(0.22, 1, 0.36, 1) both;
}
.auth-header {
display: grid;
justify-items: center;
gap: 1rem;
text-align: center;
}
.client-logo-block {
display: grid;
justify-items: center;
gap: 1rem;
width: 100%;
min-width: 0;
}
.client-logo-copy {
display: grid;
gap: 0.28rem;
min-width: 0;
justify-items: center;
}
.client-logo-copy .eyebrow {
margin: 0;
}
.client-logo-copy strong {
font-size: 1.18rem;
}
.client-logo-copy span {
color: var(--muted);
font-size: 0.88rem;
}
.hero-login-logo {
width: min(100%, 24rem);
height: auto;
display: block;
}
.footer-login-logo {
width: 12rem;
height: auto;
display: block;
}
.auth-status-pill {
display: inline-flex;
align-items: center;
padding: 0.48rem 0.8rem;
border: 1px solid color-mix(in srgb, var(--color-brand) 16%, transparent);
border-radius: 999px;
background: var(--color-brand-tint);
color: var(--color-success);
font-size: 0.78rem;
font-weight: 700;
letter-spacing: 0.04em;
text-transform: uppercase;
justify-self: center;
}
.auth-status-row,
.hero-label-row,
.version-badge {
display: inline-flex;
align-items: center;
gap: 0.55rem;
flex-wrap: wrap;
}
.auth-copy {
display: grid;
gap: 0.55rem;
}
.auth-copy h2 {
font-size: clamp(2.1rem, 4vw, 2.8rem);
line-height: 1.02;
}
.auth-copy p {
max-width: 32rem;
color: var(--muted);
font-size: 1rem;
line-height: 1.6;
}
.auth-loading-panel {
display: flex;
align-items: center;
gap: 0.95rem;
padding: 1rem 1.05rem;
border: 1px solid rgba(217, 228, 221, 0.92);
border-radius: 1.1rem;
background: rgba(248, 251, 249, 0.92);
}
.auth-loading-panel strong,
.auth-loading-panel p {
margin: 0;
}
.auth-loading-panel p {
margin-top: 0.18rem;
color: var(--muted);
}
.loading-pulse {
width: 0.95rem;
height: 0.95rem;
flex-shrink: 0;
border-radius: 999px;
background: var(--color-brand);
box-shadow: 0 0 0 0 rgba(21, 128, 61, 0.28);
animation: pulse 1.8s ease-out infinite;
}
.auth-form {
grid-template-columns: 1fr;
gap: 0.9rem;
width: 100%;
}
.field {
display: grid;
gap: 0.4rem;
}
.field span {
font-size: 0.84rem;
font-weight: 700;
color: #425248;
letter-spacing: 0.02em;
}
.auth-form input {
padding: 0.95rem 1rem;
border: 1px solid var(--color-border);
border-radius: 0.7rem;
background: var(--color-bg-app);
color: var(--color-text-primary);
transition:
border-color 160ms ease,
box-shadow 160ms ease,
background-color 160ms ease;
}
.auth-form input::placeholder {
color: var(--color-text-muted);
}
.auth-form input:focus {
outline: none;
border-color: var(--color-brand);
box-shadow: 0 0 0 3px color-mix(in srgb, var(--color-brand) 22%, transparent);
background: #fff;
}
.field-password.is-invalid input {
border-color: rgba(160, 55, 55, 0.42);
box-shadow: 0 0 0 0.24rem rgba(160, 55, 55, 0.1);
background: #fff9f9;
}
.auth-submit {
width: 100%;
min-height: 3.35rem;
margin-top: 0.2rem;
font-size: 1.02rem;
transition: background-color 160ms cubic-bezier(0.22, 1, 0.36, 1);
}
.auth-submit:hover:not(:disabled) {
background: #126a33;
}
.auth-submit:active:not(:disabled) {
background: #0f5a2b;
}
.auth-submit:focus-visible {
outline: 3px solid color-mix(in srgb, var(--color-brand) 45%, transparent);
outline-offset: 2px;
}
.auth-submit:disabled {
opacity: 0.7;
cursor: progress;
}
.auth-footer {
display: flex;
align-items: center;
justify-content: space-between;
gap: 1rem;
padding-top: 0.15rem;
border-top: 1px solid rgba(217, 228, 221, 0.88);
}
.lean-brand {
display: inline-flex;
align-items: center;
gap: 0.5rem;
}
.lean-isotipo {
width: 2.2rem;
height: 2.2rem;
object-fit: contain;
opacity: 0.8;
}
.powered-by-label {
font-size: 0.78rem;
font-weight: 500;
color: var(--muted);
}
.auth-meta {
display: grid;
justify-items: end;
gap: 0.12rem;
color: var(--muted);
font-size: 0.82rem;
}
@keyframes pulse {
0% {
box-shadow: 0 0 0 0 rgba(21, 128, 61, 0.26);
transform: scale(0.96);
}
70% {
box-shadow: 0 0 0 0.7rem rgba(21, 128, 61, 0);
transform: scale(1);
}
100% {
box-shadow: 0 0 0 0 rgba(21, 128, 61, 0);
transform: scale(0.96);
}
}
@keyframes login-card-enter {
from {
opacity: 0;
transform: translateY(10px) scale(0.988);
}
to {
opacity: 1;
transform: translateY(0) scale(1);
}
}
.dashboard-intro,
.workspace-banner,
.focus-row,
.dashboard-grid,
.operations-report,
.analysis-grid,
.detail-grid {
margin-bottom: 1.25rem;
}
.focus-row {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 0.85rem;
}
@media (max-width: 1120px) {
.focus-row { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}
@media (max-width: 720px) {
.focus-row { grid-template-columns: 1fr; }
}
.dashboard-intro h2 {
font-size: clamp(1.4rem, 2.4vw, 1.85rem);
}
.dashboard-intro,
.card-toolbar,
.metric-head,
.preview-header,
.intro-actions,
.chart-actions,
.workspace-banner {
display: flex;
align-items: center;
justify-content: space-between;
gap: 0.75rem;
}
.dashboard-intro,
.workspace-banner {
align-items: flex-end;
}
.dashboard-intro h2 {
margin: 0.3rem 0 0.35rem;
font-size: clamp(1.8rem, 3vw, 2.35rem);
font-weight: 700;
}
.greeting-row {
display: flex;
align-items: center;
gap: 1rem;
}
.greeting-icon {
display: inline-flex;
align-items: center;
justify-content: center;
width: 3rem;
height: 3rem;
color: var(--text);
flex-shrink: 0;
}
.card-toolbar p,
.metric-card p,
.preview-header p,
.support-text,
.summary-name span:last-child {
color: var(--muted);
}
.primary-button,
.secondary-button {
display: inline-flex;
align-items: center;
justify-content: center;
border-radius: 0.85rem;
padding: 0.85rem 1rem;
font-weight: 600;
text-decoration: none;
}
.primary-button {
border: none;
color: var(--color-on-brand);
background: var(--color-brand);
box-shadow: none;
}
.secondary-button {
border: 1px solid var(--color-border);
color: var(--color-text-primary);
background: var(--color-bg-surface);
}
.secondary-button.compact {
padding: 0.65rem 0.85rem;
font-size: 0.92rem;
}
.workspace-banner,
.panel-card {
background: var(--panel);
border: 1px solid var(--line);
border-radius: 1.4rem;
box-shadow: var(--shadow);
}
.workspace-banner,
.panel-card {
padding: 1.2rem;
}
.operations-report {
position: relative;
overflow: hidden;
padding: 1.2rem;
border: 1px solid var(--color-border);
border-radius: 1.4rem;
background: var(--color-bg-surface);
box-shadow: var(--shadow);
}
.operations-toolbar {
margin-bottom: 0.9rem;
}
.operations-heading {
display: flex;
align-items: center;
gap: 0.85rem;
min-width: 0;
}
.operations-heading h3 {
margin: 0 0 0.18rem;
color: var(--text);
}
.operations-heading p {
margin: 0;
color: var(--muted);
}
.operations-icon {
display: inline-flex;
align-items: center;
justify-content: center;
width: 3.1rem;
height: 3.1rem;
flex-shrink: 0;
border: 1px solid rgba(21, 128, 61, 0.18);
border-radius: 1rem;
background: #f0f8f3;
color: #0f6f3d;
box-shadow: inset 0 -0.45rem 1rem rgba(21, 128, 61, 0.08);
}
.operations-graphic {
display: grid;
gap: 0.5rem;
margin-bottom: 0.95rem;
padding: 0.75rem 0.85rem;
border: 1px solid rgba(214, 228, 220, 0.86);
border-radius: 1rem;
background: rgba(255, 255, 255, 0.62);
box-shadow: inset 0 -0.6rem 1.4rem rgba(21, 128, 61, 0.06);
}
.operations-graphic-labels,
.operations-graphic-track {
display: grid;
grid-template-columns: 2.25rem minmax(4rem, 1fr) 4.5rem 2.25rem;
align-items: center;
gap: 0.45rem;
}
.operations-graphic-labels {
grid-template-columns: repeat(3, minmax(0, 1fr));
color: #365243;
font-size: 0.72rem;
font-weight: 800;
letter-spacing: 0.04em;
text-transform: uppercase;
}
.operations-graphic-labels span:nth-child(2) {
text-align: center;
}
.operations-graphic-labels span:nth-child(3) {
text-align: right;
}
.operation-node {
display: block;
width: 2.25rem;
height: 2.25rem;
border-radius: 0.8rem;
background: #0f6f3d;
box-shadow: inset 0 -0.35rem 0.65rem rgba(0, 0, 0, 0.12);
}
.pricing-node {
background: #e7ad3c;
}
.operation-track {
align-self: center;
height: 0.24rem;
border-radius: 999px;
background: linear-gradient(90deg, rgba(21, 128, 61, 0.22), rgba(59, 130, 196, 0.58), rgba(231, 173, 60, 0.55));
}
.operation-bars {
display: flex;
align-items: end;
justify-content: center;
gap: 0.28rem;
height: 2.4rem;
}
.operation-bars i {
display: block;
width: 0.7rem;
border-radius: 999px 999px 0.25rem 0.25rem;
background: #3b82c4;
}
.operation-bars i:nth-child(1) {
height: 1.2rem;
}
.operation-bars i:nth-child(2) {
height: 2rem;
background: #15803d;
}
.operation-bars i:nth-child(3) {
height: 1.55rem;
background: #e7ad3c;
}
.operations-link {
gap: 0.45rem;
white-space: nowrap;
}
.operations-metrics {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 0.8rem;
margin-bottom: 1rem;
}
.operations-metrics article {
position: relative;
overflow: hidden;
padding: 0.95rem;
border: 1px solid rgba(214, 228, 220, 0.94);
border-radius: 1rem;
background: rgba(255, 255, 255, 0.84);
box-shadow: 0 0.75rem 1.4rem rgba(43, 57, 47, 0.05);
}
.operations-metrics article::after {
content: '';
position: absolute;
right: 0.85rem;
bottom: 0;
left: 0.85rem;
height: 0.24rem;
border-radius: 999px 999px 0 0;
background: #15803d;
opacity: 0.65;
}
.operations-metrics article.bags::after {
background: #3b82c4;
}
.operations-metrics article.value::after {
background: #e7ad3c;
}
.operations-metrics article.warning {
border-color: rgba(231, 173, 60, 0.44);
background: #fff8e8;
}
.operations-metrics article.warning::after {
background: #b45309;
}
.metric-label {
display: flex;
align-items: center;
justify-content: space-between;
gap: 0.65rem;
}
.metric-symbol {
display: inline-flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
color: #0f6f3d;
}
.bags .metric-symbol {
color: #2f6f9f;
}
.value .metric-symbol {
color: #9a6718;
}
.warning .metric-symbol {
color: #9a3412;
}
.metric-label span,
.operations-metrics p,
.mini-heading span,
.report-list small {
color: var(--muted);
}
.operations-metrics strong {
display: block;
margin: 0.4rem 0 0.25rem;
font-size: 1.55rem;
line-height: 1;
}
.operations-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 1rem;
}
.operations-grid > article {
padding-top: 0.9rem;
border-top: 1px solid var(--line);
}
.mini-heading,
.report-list div {
display: flex;
align-items: center;
justify-content: space-between;
gap: 0.8rem;
}
.mini-heading {
margin-bottom: 0.65rem;
}
.report-list {
display: grid;
gap: 0.55rem;
}
.report-list div {
padding: 0.65rem 0;
border-bottom: 1px solid var(--line);
}
.report-list div:last-child {
border-bottom: none;
}
.report-list strong,
.report-list small {
display: block;
}
.report-list em {
flex-shrink: 0;
font-style: normal;
font-weight: 700;
}
.focus-grid {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 0.85rem;
min-width: min(100%, 42rem);
}
.login-banner {
align-items: center;
}
.loading-banner {
min-height: 11rem;
}
.signin-form {
display: grid;
grid-template-columns: 1fr;
gap: 0.95rem;
width: 100%;
}
.login-error {
margin: 0;
padding: 0.75rem 0.95rem;
border: 1px solid rgba(160, 55, 55, 0.3);
border-radius: 0.7rem;
background: #fdf2f2;
color: #8a1622;
font-weight: 600;
font-size: 0.95rem;
}
.focus-card {
display: grid;
gap: 0.4rem;
padding: 0.95rem 1rem;
border-radius: 1rem;
border: 1px solid var(--line);
background: var(--panel-soft);
}
.focus-card.positive {
background: var(--color-success-tint);
}
.focus-card.warning {
background: var(--color-warning-tint);
}
.focus-card.neutral {
background: var(--panel-soft);
}
.focus-code {
width: 2rem;
height: 2rem;
display: inline-flex;
align-items: center;
justify-content: center;
border-radius: 0.75rem;
color: var(--color-on-brand);
background: var(--color-brand);
font-size: 0.74rem;
font-weight: 700;
letter-spacing: 0.05em;
}
.focus-card strong,
.market-card h3,
.gauge-card h3,
.metric-card strong,
.summary-name strong,
.preview-facts strong {
font-weight: 700;
}
.focus-card span {
color: var(--muted);
font-size: 0.84rem;
}
.focus-card em {
font-style: normal;
font-size: 1rem;
font-weight: 700;
}
.dashboard-grid {
display: grid;
grid-template-columns: minmax(0, 1.12fr) minmax(0, 1.02fr) minmax(16rem, 0.76fr);
grid-template-areas: 'market gauge metrics';
gap: 1rem;
align-items: stretch;
}
.analysis-grid {
display: grid;
grid-template-columns: minmax(0, 1.35fr) minmax(320px, 0.95fr);
gap: 1rem;
}
.detail-grid {
display: grid;
grid-template-columns: minmax(0, 1.35fr) minmax(280px, 0.85fr);
gap: 1rem;
}
.market-card {
grid-area: market;
}
.gauge-card {
grid-area: gauge;
}
.card-toolbar {
align-items: flex-start;
margin-bottom: 1rem;
}
.card-toolbar h3 {
font-size: 1.15rem;
font-weight: 700;
}
.pill,
.toggle-pill {
display: inline-flex;
align-items: center;
border-radius: 999px;
}
.pill {
padding: 0.48rem 0.8rem;
font-size: 0.9rem;
font-weight: 600;
}
.pill.success {
color: var(--green-deep);
background: var(--green-soft);
}
.toggle-pill {
gap: 0.3rem;
padding: 0.25rem;
background: var(--panel-soft);
}
.toggle-pill span {
width: 2.2rem;
height: 1.8rem;
display: inline-flex;
align-items: center;
justify-content: center;
border-radius: 999px;
color: var(--muted);
font-size: 0.72rem;
font-weight: 700;
}
.toggle-pill .active {
color: var(--color-text-primary);
background: var(--color-bg-surface);
border: 1px solid var(--color-border);
}
.market-layout {
display: flex;
align-items: center;
justify-content: space-between;
gap: 1rem;
min-height: 13rem;
}
.market-card h3 {
font-size: 2rem;
margin-bottom: 0.35rem;
}
.market-card p {
color: var(--muted);
}
.hero-value {
margin: 1.6rem 0 0.35rem;
font-size: 3rem;
font-weight: 700;
line-height: 1;
}
.support-text {
display: flex;
flex-wrap: wrap;
gap: 0.6rem;
font-size: 0.95rem;
}
.field-emblem {
position: relative;
width: 8.5rem;
height: 8.5rem;
flex-shrink: 0;
border-radius: 1.4rem;
background: linear-gradient(
150deg,
var(--color-brand) 0%,
color-mix(in srgb, var(--color-brand) 70%, var(--color-text-primary)) 100%
);
overflow: hidden;
}
.sun-core {
position: absolute;
top: 1.05rem;
right: 1rem;
width: 2.9rem;
height: 2.9rem;
border-radius: 999px;
background: radial-gradient(
circle at 38% 38%,
color-mix(in srgb, var(--color-on-brand) 38%, transparent) 0%,
color-mix(in srgb, var(--color-on-brand) 12%, transparent) 62%,
transparent 72%
);
}
.field-stripe {
position: absolute;
left: -10%;
right: -10%;
height: 22%;
border-radius: 999px;
background: color-mix(in srgb, var(--color-on-brand) 16%, transparent);
transform: rotate(-18deg);
}
.field-stripe.one {
bottom: 2.4rem;
}
.field-stripe.two {
bottom: 1.35rem;
}
.field-stripe.three {
bottom: 0.3rem;
}
.gauge-visual {
position: relative;
height: 14rem;
}
.gauge-visual svg {
width: 100%;
height: 100%;
}
.gauge-center {
position: absolute;
left: 50%;
bottom: 2.2rem;
display: grid;
justify-items: center;
transform: translateX(-50%);
}
.gauge-center strong {
font-size: 2.15rem;
font-weight: 700;
}
.gauge-center span,
.legend-row span {
color: var(--muted);
font-size: 0.92rem;
}
.legend-row {
display: flex;
flex-wrap: wrap;
gap: 1rem;
}
.legend-row span {
display: inline-flex;
align-items: center;
gap: 0.4rem;
}
.legend-row i {
width: 0.55rem;
height: 0.55rem;
border-radius: 999px;
}
.metric-stack {
grid-area: metrics;
display: grid;
gap: 1rem;
}
.metric-card {
min-height: 10rem;
}
.metric-head {
margin-bottom: 1rem;
color: var(--muted);
font-size: 0.95rem;
}
.metric-icon {
width: 2rem;
height: 2rem;
border-radius: 999px;
background: var(--color-brand-tint);
border: 1px solid color-mix(in srgb, var(--color-brand) 15%, transparent);
}
.metric-card strong {
display: block;
margin-bottom: 0.45rem;
font-size: 2rem;
}
.chart-shell {
position: relative;
height: 18rem;
margin-top: 0.4rem;
border-radius: 1.25rem;
background:
linear-gradient(180deg, rgba(21, 128, 61, 0.05) 0%, rgba(21, 128, 61, 0.01) 100%),
repeating-linear-gradient(
to bottom,
transparent 0 3.45rem,
rgba(21, 128, 61, 0.08) 3.45rem 3.55rem
);
overflow: hidden;
}
.chart-shell svg {
width: 100%;
height: 100%;
}
.focus-badge {
position: absolute;
top: 0.9rem;
transform: translateX(-50%);
display: grid;
gap: 0.1rem;
padding: 0.45rem 0.6rem;
border-radius: 0.8rem;
background: #1e2420;
color: #fff;
box-shadow: none;
}
.focus-badge span {
font-size: 0.72rem;
opacity: 0.76;
}
.focus-badge strong {
font-size: 0.92rem;
}
.month-row {
display: grid;
grid-template-columns: repeat(9, minmax(0, 1fr));
gap: 0.4rem;
margin-top: 0.85rem;
color: #8b9a91;
font-size: 0.84rem;
}
.orchard-visual {
position: relative;
height: 16.5rem;
border-radius: 1.2rem;
overflow: hidden;
background: linear-gradient(180deg, #a7dafc 0%, #e5f4ff 42%, #6eb857 42%, #2d6d28 100%);
}
.orchard-sky,
.orchard-sun,
.orchard-hill,
.orchard-row {
position: absolute;
}
.orchard-sun {
top: 1.4rem;
right: 1.6rem;
width: 4.5rem;
height: 4.5rem;
border-radius: 999px;
background: radial-gradient(circle, #fff6c7 0%, #ffd567 58%, rgba(255, 213, 103, 0.18) 72%, transparent 73%);
}
.orchard-hill {
left: -8%;
right: -8%;
bottom: 30%;
height: 24%;
background: linear-gradient(180deg, rgba(66, 127, 50, 0.7), rgba(45, 94, 37, 0.95));
clip-path: polygon(0 75%, 18% 48%, 35% 61%, 56% 32%, 74% 54%, 100% 25%, 100% 100%, 0 100%);
}
.orchard-row {
bottom: -8%;
width: 38%;
height: 48%;
background: repeating-linear-gradient(
180deg,
rgba(60, 133, 43, 0.95) 0 14px,
rgba(47, 107, 34, 0.95) 14px 28px
);
border-radius: 50% 50% 0 0;
}
.orchard-row.left {
left: -4%;
transform: rotate(8deg);
}
.orchard-row.center {
left: 31%;
}
.orchard-row.right {
right: -4%;
transform: rotate(-8deg);
}
.preview-body {
margin-top: -1.4rem;
position: relative;
z-index: 1;
padding: 1rem;
border: 1px solid var(--line);
border-radius: 1.2rem;
background: rgba(255, 255, 255, 0.96);
}
.preview-header a {
padding: 0.65rem 0.8rem;
border-radius: 0.8rem;
border: 1px solid var(--line-strong);
background: #fff;
}
.preview-facts {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 0.75rem;
margin-top: 1rem;
}
.preview-facts article {
padding: 0.85rem;
border-radius: 0.95rem;
background: var(--panel-soft);
border: 1px solid var(--line);
}
.preview-facts span {
display: block;
margin-bottom: 0.35rem;
color: var(--muted);
font-size: 0.84rem;
}
.table-wrap {
overflow-x: auto;
}
table {
width: 100%;
min-width: 46rem;
border-collapse: separate;
border-spacing: 0 0.7rem;
}
th,
td {
padding: 1rem 1rem;
text-align: left;
white-space: nowrap;
}
th {
color: var(--muted);
font-size: 0.78rem;
font-weight: 600;
letter-spacing: 0.06em;
text-transform: uppercase;
}
tbody td {
background: var(--panel-soft);
border-top: 1px solid var(--line);
border-bottom: 1px solid var(--line);
color: #213029;
}
tbody td:first-child {
border-left: 1px solid var(--line);
border-radius: 1rem 0 0 1rem;
}
tbody td:last-child {
border-right: 1px solid var(--line);
border-radius: 0 1rem 1rem 0;
}
.task-cell {
min-width: 18rem;
}
.table-item,
.owner-chip,
.due-block {
display: flex;
align-items: center;
gap: 0.8rem;
}
.table-item strong,
.owner-chip strong,
.due-block strong {
display: block;
font-size: 0.96rem;
}
.table-item span:last-child,
.due-block span {
display: block;
color: var(--muted);
font-size: 0.82rem;
margin-top: 0.18rem;
}
.task-icon,
.owner-chip span {
width: 2.3rem;
height: 2.3rem;
display: inline-flex;
align-items: center;
justify-content: center;
border-radius: 0.8rem;
font-size: 0.72rem;
font-weight: 700;
letter-spacing: 0.05em;
flex-shrink: 0;
}
.task-icon.positive {
color: var(--green-deep);
background: #e8f7ee;
}
.task-icon.warning {
color: #b16b1e;
background: #fff3e3;
}
.task-icon.neutral {
color: #54675e;
background: #edf2ef;
}
.owner-chip span {
color: #fff;
background: var(--green-deep);
border-radius: 999px;
}
.due-block {
display: grid;
gap: 0.1rem;
}
.status-chip {
display: inline-flex;
align-items: center;
justify-content: center;
border-radius: 999px;
padding: 0.38rem 0.72rem;
font-size: 0.84rem;
font-weight: 600;
text-transform: capitalize;
}
.status-chip.positive {
color: var(--green-deep);
background: var(--green-soft);
}
.status-chip.warning {
color: #a9681d;
background: #fff6e6;
}
.status-chip.neutral {
color: #52635a;
background: #eef3f0;
}
.summary-list {
display: grid;
gap: 0.7rem;
}
.summary-list article {
display: flex;
align-items: center;
justify-content: space-between;
gap: 0.75rem;
padding: 0.9rem 0;
border-bottom: 1px solid var(--line);
}
.summary-list article:last-child {
border-bottom: none;
padding-bottom: 0;
}
.summary-name {
display: flex;
align-items: center;
gap: 0.7rem;
}
.summary-dot {
width: 0.75rem;
height: 0.75rem;
border-radius: 999px;
background: var(--color-brand);
flex-shrink: 0;
}
@media (max-width: 1320px) {
.workspace-banner {
align-items: stretch;
}
.focus-grid {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.dashboard-grid {
grid-template-columns: repeat(2, minmax(0, 1fr));
grid-template-areas:
'market gauge'
'metrics metrics';
}
.metric-stack {
grid-template-columns: repeat(3, minmax(0, 1fr));
}
}
@media (max-width: 1120px) {
.analysis-grid,
.detail-grid,
.operations-grid {
grid-template-columns: 1fr;
}
.operations-link {
justify-self: start;
}
.focus-grid {
min-width: 0;
}
}
@media (max-width: 900px) {
.dashboard-grid {
grid-template-columns: 1fr;
grid-template-areas:
'market'
'gauge'
'metrics';
}
.metric-stack {
grid-template-columns: 1fr;
}
.focus-grid {
grid-template-columns: 1fr;
}
.operations-metrics {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
}
@media (prefers-reduced-motion: reduce) {
.auth-card-login {
animation: none;
}
}
@media (max-width: 860px) {
.auth-stage {
min-height: auto;
padding: 0.4rem 0 0.8rem;
}
.auth-card {
padding: 1.5rem 1.15rem 1.15rem;
border-radius: 1.1rem;
}
.auth-header,
.auth-footer {
flex-direction: column;
align-items: flex-start;
}
.auth-copy h2 {
font-size: 1.9rem;
}
.auth-meta {
justify-items: start;
}
.dashboard-intro,
.intro-actions,
.workspace-banner,
.card-toolbar,
.metric-head,
.preview-header,
.chart-actions,
.market-layout {
flex-direction: column;
align-items: flex-start;
}
.preview-facts,
.operations-metrics,
.signin-form {
grid-template-columns: 1fr;
}
.operations-graphic {
width: 100%;
}
.field-emblem {
width: 6.5rem;
height: 6.5rem;
}
.hero-value {
font-size: 2.4rem;
}
.month-row {
grid-template-columns: repeat(3, minmax(0, 1fr));
row-gap: 0.5rem;
}
}
@media (max-width: 760px) {
.workspace-banner,
.panel-card,
.preview-body {
padding: 1rem;
}
table,
thead,
tbody,
tr,
td {
display: block;
width: 100%;
}
table {
min-width: 0;
border-spacing: 0;
}
thead {
display: none;
}
tbody {
display: grid;
gap: 0.9rem;
}
tbody tr {
padding: 0.3rem;
border: 1px solid var(--line);
border-radius: 1rem;
background: var(--panel-soft);
}
tbody td {
padding: 0.78rem 0.8rem;
white-space: normal;
border: none;
border-radius: 0;
background: transparent;
}
tbody td:first-child,
tbody td:last-child {
border: none;
border-radius: 0;
}
tbody td + td {
border-top: 1px solid var(--line);
}
tbody td::before {
content: attr(data-label);
display: block;
margin-bottom: 0.35rem;
color: var(--muted);
font-size: 0.72rem;
font-weight: 700;
letter-spacing: 0.06em;
text-transform: uppercase;
}
.task-cell {
min-width: 0;
}
.table-item,
.owner-chip {
align-items: flex-start;
}
}
</style>