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:
Joel Brock
2026-05-09 21:48:45 -07:00
parent dcdf315244
commit 656bf7fd0a
9 changed files with 776 additions and 272 deletions

View File

@@ -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%;
}

View File

@@ -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>
);
}

View File

@@ -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>