Visual identity: Field Almanac — Fraunces+DM Sans, OKLCH cream/leaf/clay palette, paper texture, hand-drawn stage icons, draft auto-save, stage progress dots
This commit is contained in:
184
app/globals.css
184
app/globals.css
@@ -1,84 +1,172 @@
|
||||
@import "tailwindcss";
|
||||
|
||||
/* ─────────────────────────────────────────────────────────────────────────
|
||||
* Theme — Food Co-op Initiative palette
|
||||
* Theme — "Field Almanac"
|
||||
*
|
||||
* "leaf" is the primary green. Inspired by the warm, grassroots-organic
|
||||
* feel of fci.coop: not corporate teal, not jewel-toned. A balanced
|
||||
* field-green that sits comfortably alongside warm cream/stone neutrals.
|
||||
* Inspiration: 1970s ecological-movement print, agricultural cooperative
|
||||
* publications, hand-bound field journals. Warm cream paper, deep
|
||||
* botanical green ink, sparing terracotta accent for emphasis.
|
||||
*
|
||||
* "stone" is the neutral ramp — slightly warm, never icy.
|
||||
*
|
||||
* Both are built as Tailwind 4 color tokens, so utility classes like
|
||||
* `bg-leaf-700`, `text-stone-600` work everywhere.
|
||||
* The palette is built in OKLCH for perceptual uniformity. Every neutral
|
||||
* is tinted toward the leaf hue (h≈130) so cream and green feel like they
|
||||
* came from the same press run.
|
||||
* ─────────────────────────────────────────────────────────────────────── */
|
||||
|
||||
@theme {
|
||||
--font-sans: var(--font-sans), ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
||||
--font-serif: var(--font-serif), ui-serif, Georgia, Cambria, "Times New Roman", Times, serif;
|
||||
/* Typography */
|
||||
--font-display: var(--font-display), ui-serif, Georgia, "Times New Roman", serif;
|
||||
--font-body: var(--font-body), ui-sans-serif, system-ui, -apple-system, sans-serif;
|
||||
|
||||
--color-leaf-50: #f3f8f0;
|
||||
--color-leaf-100: #e3eed9;
|
||||
--color-leaf-200: #c8dcb3;
|
||||
--color-leaf-300: #a3c280;
|
||||
--color-leaf-400: #7ea455;
|
||||
--color-leaf-500: #5e8638;
|
||||
--color-leaf-600: #4a6c2a;
|
||||
--color-leaf-700: #3a5520;
|
||||
--color-leaf-800: #2c401a;
|
||||
--color-leaf-900: #1f2d12;
|
||||
/* Custom utility names */
|
||||
--color-paper: oklch(97.5% 0.012 95); /* warm cream */
|
||||
--color-paper-2: oklch(95.5% 0.018 90); /* deeper cream — section bands */
|
||||
--color-ink: oklch(20% 0.02 130); /* near-black, slight green tint */
|
||||
--color-ink-soft: oklch(35% 0.018 130);
|
||||
--color-ink-mute: oklch(55% 0.014 120);
|
||||
--color-rule: oklch(82% 0.025 100); /* hairline rules — like ink on cream */
|
||||
--color-rule-soft: oklch(89% 0.02 100);
|
||||
|
||||
--color-stone-50: #fafaf7;
|
||||
--color-stone-100: #f3f2ed;
|
||||
--color-stone-200: #e7e4dc;
|
||||
--color-stone-300: #d4cfc1;
|
||||
--color-stone-400: #aaa494;
|
||||
--color-stone-500: #807a6a;
|
||||
--color-stone-600: #5b574b;
|
||||
--color-stone-700: #403d35;
|
||||
--color-stone-800: #2a2823;
|
||||
--color-stone-900: #1a1815;
|
||||
/* Botanical green — primary */
|
||||
--color-leaf-50: oklch(97% 0.025 135);
|
||||
--color-leaf-100: oklch(93% 0.05 135);
|
||||
--color-leaf-200: oklch(87% 0.085 135);
|
||||
--color-leaf-300: oklch(78% 0.115 135);
|
||||
--color-leaf-400: oklch(68% 0.13 135);
|
||||
--color-leaf-500: oklch(57% 0.135 135);
|
||||
--color-leaf-600: oklch(47% 0.13 135);
|
||||
--color-leaf-700: oklch(38% 0.115 135);
|
||||
--color-leaf-800: oklch(30% 0.09 135);
|
||||
--color-leaf-900: oklch(22% 0.06 135);
|
||||
|
||||
/* Terracotta — accent. Used sparingly: current-stage marker, key CTAs. */
|
||||
--color-clay-100: oklch(94% 0.04 50);
|
||||
--color-clay-200: oklch(86% 0.075 50);
|
||||
--color-clay-400: oklch(70% 0.13 45);
|
||||
--color-clay-500: oklch(63% 0.15 42);
|
||||
--color-clay-600: oklch(54% 0.155 40);
|
||||
--color-clay-700: oklch(45% 0.135 38);
|
||||
|
||||
/* Stone — kept as a familiar alias mapping to our warm neutrals so any
|
||||
* earlier `text-stone-*` / `border-stone-*` references stay readable. */
|
||||
--color-stone-50: var(--color-paper);
|
||||
--color-stone-100: var(--color-paper-2);
|
||||
--color-stone-200: var(--color-rule-soft);
|
||||
--color-stone-300: var(--color-rule);
|
||||
--color-stone-400: oklch(70% 0.018 110);
|
||||
--color-stone-500: var(--color-ink-mute);
|
||||
--color-stone-600: oklch(48% 0.015 120);
|
||||
--color-stone-700: var(--color-ink-soft);
|
||||
--color-stone-800: oklch(28% 0.018 130);
|
||||
--color-stone-900: var(--color-ink);
|
||||
}
|
||||
|
||||
/* Base / reset */
|
||||
/* Base — paper texture and ink defaults */
|
||||
:root {
|
||||
color-scheme: light;
|
||||
--background: var(--color-stone-50);
|
||||
--foreground: var(--color-stone-900);
|
||||
}
|
||||
|
||||
html {
|
||||
-webkit-text-size-adjust: 100%;
|
||||
font-feature-settings: "kern", "liga", "calt", "ss01";
|
||||
}
|
||||
|
||||
body {
|
||||
background: var(--background);
|
||||
color: var(--foreground);
|
||||
background-color: var(--color-paper);
|
||||
color: var(--color-ink);
|
||||
/* Subtle paper grain — two diagonal repeating linear gradients combined
|
||||
* with a soft radial overlay. No external image. Survives dark mode if
|
||||
* we ever add one. */
|
||||
background-image:
|
||||
radial-gradient(ellipse 90% 60% at 50% 0%, oklch(99% 0.01 95 / 0.7), transparent 60%),
|
||||
repeating-linear-gradient(
|
||||
105deg,
|
||||
oklch(96% 0.02 95 / 0.4) 0,
|
||||
oklch(96% 0.02 95 / 0.4) 1px,
|
||||
transparent 1px,
|
||||
transparent 6px
|
||||
),
|
||||
repeating-linear-gradient(
|
||||
15deg,
|
||||
oklch(94% 0.02 95 / 0.25) 0,
|
||||
oklch(94% 0.02 95 / 0.25) 1px,
|
||||
transparent 1px,
|
||||
transparent 9px
|
||||
);
|
||||
}
|
||||
|
||||
/* Larger, more readable form controls on small screens */
|
||||
input,
|
||||
select,
|
||||
textarea,
|
||||
button {
|
||||
/* Typography defaults */
|
||||
.font-display { font-family: var(--font-display); font-feature-settings: "ss01", "cv01"; }
|
||||
.font-body { font-family: var(--font-body); }
|
||||
|
||||
/* Long-form headings get optical-size tuning for richer letterforms */
|
||||
h1.font-display, h2.font-display, h3.font-display {
|
||||
font-variation-settings: "opsz" 144, "SOFT" 50;
|
||||
letter-spacing: -0.015em;
|
||||
}
|
||||
|
||||
/* Forms: shared input rhythm */
|
||||
input, select, textarea, button {
|
||||
font: inherit;
|
||||
}
|
||||
|
||||
/* Ensure focus is always perceptible (in addition to Tailwind's ring) */
|
||||
/* A more deliberate focus ring — uses the leaf token */
|
||||
*:focus-visible {
|
||||
outline: 2px solid transparent;
|
||||
outline: 2px solid var(--color-leaf-500);
|
||||
outline-offset: 2px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
/* Make Chrome's date input chevron less garish */
|
||||
/* Date input chevron styled to match */
|
||||
input[type="date"]::-webkit-calendar-picker-indicator {
|
||||
opacity: 0.6;
|
||||
opacity: 0.5;
|
||||
cursor: pointer;
|
||||
filter: hue-rotate(70deg);
|
||||
}
|
||||
|
||||
/* Prevent layout shift when the sticky submit bar appears */
|
||||
@media (min-width: 640px) {
|
||||
main {
|
||||
padding-bottom: 5rem;
|
||||
/* Print: lay it out like a real field-journal page when a user hits Cmd+P */
|
||||
@media print {
|
||||
body { background: white; color: black; }
|
||||
body { background-image: none; }
|
||||
details { display: block !important; }
|
||||
details > div[role="region"] { display: block !important; }
|
||||
.no-print { display: none !important; }
|
||||
}
|
||||
|
||||
/* Reduced motion: defer to user preference */
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
*, *::before, *::after {
|
||||
animation-duration: 0.01ms !important;
|
||||
animation-iteration-count: 1 !important;
|
||||
transition-duration: 0.01ms !important;
|
||||
scroll-behavior: auto !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* A small cohort of utility classes — rule lines, rough underlines, etc. */
|
||||
.rule-soft {
|
||||
background: linear-gradient(
|
||||
90deg,
|
||||
transparent 0%,
|
||||
var(--color-rule) 12%,
|
||||
var(--color-rule) 88%,
|
||||
transparent 100%
|
||||
);
|
||||
}
|
||||
|
||||
/* Ink-bloom focus — used on text inputs to feel handwritten */
|
||||
.input-ink {
|
||||
background-image: linear-gradient(
|
||||
to top,
|
||||
var(--color-leaf-200) 0%,
|
||||
var(--color-leaf-200) 1px,
|
||||
transparent 1px,
|
||||
transparent 100%
|
||||
);
|
||||
background-size: 0% 100%;
|
||||
background-repeat: no-repeat;
|
||||
background-position: 0 100%;
|
||||
transition: background-size 280ms cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
.input-ink:focus {
|
||||
background-size: 100% 100%;
|
||||
}
|
||||
|
||||
@@ -1,22 +1,36 @@
|
||||
import type { Metadata } from "next";
|
||||
import { Inter, Source_Serif_4 } from "next/font/google";
|
||||
import { Fraunces, DM_Sans } from "next/font/google";
|
||||
import "./globals.css";
|
||||
|
||||
const inter = Inter({
|
||||
variable: "--font-sans",
|
||||
/**
|
||||
* Display face: Fraunces. A variable serif with strong optical interest —
|
||||
* elegant in long descenders and bracketed serifs, reads like a field guide
|
||||
* or almanac. We use the SOFT axis to round the inktraps slightly so it
|
||||
* doesn't read as "old print" but as "warm modern editorial."
|
||||
*/
|
||||
const display = Fraunces({
|
||||
variable: "--font-display",
|
||||
subsets: ["latin"],
|
||||
axes: ["SOFT", "opsz"],
|
||||
display: "swap",
|
||||
});
|
||||
|
||||
const serif = Source_Serif_4({
|
||||
variable: "--font-serif",
|
||||
/**
|
||||
* Body face: DM Sans. A humanist geometric sans with a slightly soft feel —
|
||||
* legible at small sizes for forms, doesn't read as cold/corporate the way
|
||||
* Inter or Helvetica does.
|
||||
*/
|
||||
const body = DM_Sans({
|
||||
variable: "--font-body",
|
||||
subsets: ["latin"],
|
||||
display: "swap",
|
||||
});
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Co-op Check-in · Food Co-op Initiative",
|
||||
description: "Update your co-op's progress through the FCI organizing framework.",
|
||||
description:
|
||||
"Update your co-op's progress through the FCI organizing framework. A monthly check-in for food co-ops in development.",
|
||||
robots: { index: false, follow: false }, // Form pages are tokenized; not for crawlers.
|
||||
};
|
||||
|
||||
export default function RootLayout({
|
||||
@@ -27,9 +41,11 @@ export default function RootLayout({
|
||||
return (
|
||||
<html
|
||||
lang="en"
|
||||
className={`${inter.variable} ${serif.variable} h-full antialiased`}
|
||||
className={`${display.variable} ${body.variable} h-full antialiased`}
|
||||
>
|
||||
<body className="min-h-full flex flex-col font-sans">{children}</body>
|
||||
<body className="min-h-full flex flex-col font-body bg-paper text-ink">
|
||||
{children}
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
|
||||
54
app/page.tsx
54
app/page.tsx
@@ -12,24 +12,16 @@ export default async function Page({ searchParams }: PageProps) {
|
||||
|
||||
return (
|
||||
<>
|
||||
<a
|
||||
href="#main"
|
||||
className="sr-only focus:not-sr-only focus:absolute focus:left-4 focus:top-4 focus:z-50 focus:rounded focus:bg-paper focus:px-3 focus:py-2 focus:text-ink focus:shadow"
|
||||
>
|
||||
Skip to content
|
||||
</a>
|
||||
<SiteHeader />
|
||||
<main id="main" className="flex-1 bg-stone-50">
|
||||
<a
|
||||
href="#main"
|
||||
className="sr-only focus:not-sr-only focus:absolute focus:left-4 focus:top-4 focus:rounded focus:bg-white focus:px-3 focus:py-2 focus:shadow"
|
||||
>
|
||||
Skip to content
|
||||
</a>
|
||||
<div className="mx-auto max-w-3xl px-4 py-8 sm:px-6 sm:py-12">
|
||||
<header className="mb-8">
|
||||
<h1 className="text-2xl font-semibold text-stone-900 sm:text-3xl">
|
||||
{formConfig.title}
|
||||
</h1>
|
||||
{formConfig.subtitle && (
|
||||
<p className="mt-2 text-stone-700 leading-relaxed">{formConfig.subtitle}</p>
|
||||
)}
|
||||
</header>
|
||||
|
||||
<main id="main" className="flex-1">
|
||||
<div className="mx-auto max-w-3xl px-4 py-10 sm:px-6 sm:py-14">
|
||||
<PageIntro />
|
||||
{!cid || !cs ? (
|
||||
<MissingLinkParams />
|
||||
) : (
|
||||
@@ -44,14 +36,30 @@ export default async function Page({ searchParams }: PageProps) {
|
||||
);
|
||||
}
|
||||
|
||||
function PageIntro() {
|
||||
return (
|
||||
<header className="mb-10 max-w-2xl">
|
||||
<p className="text-[11px] uppercase tracking-[0.18em] text-leaf-700">
|
||||
Monthly check-in · Co-op organizing
|
||||
</p>
|
||||
<h1 className="mt-2 font-display text-[40px] font-normal leading-[1.05] tracking-tight text-ink sm:text-[52px]">
|
||||
{formConfig.title}
|
||||
</h1>
|
||||
<p className="mt-4 max-w-prose text-[17px] leading-relaxed text-ink-soft">
|
||||
{formConfig.subtitle}
|
||||
</p>
|
||||
<div className="mt-6 h-px bg-rule" />
|
||||
</header>
|
||||
);
|
||||
}
|
||||
|
||||
function MissingLinkParams() {
|
||||
return (
|
||||
<div
|
||||
role="alert"
|
||||
className="rounded-xl border border-amber-200 bg-amber-50 px-6 py-6 text-amber-900"
|
||||
>
|
||||
<h2 className="text-lg font-semibold">This link is missing required information.</h2>
|
||||
<p className="mt-2 leading-relaxed">
|
||||
<div role="alert" className="rounded-lg border-2 border-clay-200 bg-clay-100/30 px-6 py-7">
|
||||
<h2 className="font-display text-xl font-medium text-clay-700">
|
||||
This link is missing required information.
|
||||
</h2>
|
||||
<p className="mt-3 leading-relaxed text-ink-soft">
|
||||
Open the form using the personalized link from your email. If you no longer have it,
|
||||
please contact your engagement coordinator and ask for a fresh link.
|
||||
</p>
|
||||
|
||||
Reference in New Issue
Block a user