2494 lines
57 KiB
Svelte
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>© {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>© {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>
|