Adopt the Stitch design system

Google Stitch produced an Atelier Dining & Hospitality system for this app: warm
ivory linen and espresso for the guest surfaces, burnished gold for the single
active thing, Playfair Display for the restaurant's voice and Plus Jakarta Sans
for everything transactional, editorial rows with dot leaders, pill controls,
hairline containment instead of drop shadows. The four screens and the system
document are kept in docs/design/stitch/ as the source.

Both fonts are now self-hosted with their licences, the guest stylesheet is
rebuilt on the system, and the admin is the same system in its dark mode
(espresso workspace, obsidian pods, gold actions) by overriding the same tokens
at a higher specificity, since the per-Site theme is injected inline after the
stylesheets. The Site theme in config/ moves to the system's palette, which is
where per-Site branding belongs; the old Archivo font is dropped.
This commit is contained in:
tanshu committed 2026-09-22 23:07:38 +05:30
1 parent a29c5c4b0c
commit b13042f0db
17 files changed
+679 -191

No files matched your search

+41 -11
View File
@@ -6,19 +6,49 @@
"logo": "/assets/favicon.svg"
},
"theme": {
"steel": "#14181b",
"rail": "#1f262b",
"paper": "#f2efe9",
"accent": "#0088aa",
"copper": "#b2703b"
"steel": "#faf8f5",
"rail": "#f4f1ea",
"paper": "#ffffff",
"accent": "#c59b58",
"copper": "#b08544"
},
"sections": [
{ "slug": "food", "label": "Food", "shortLabel": "Food", "order": 1 },
{ "slug": "bar", "label": "Bar", "shortLabel": "Bar", "order": 2 },
{ "slug": "wine", "label": "Wine", "shortLabel": "Wine", "order": 3 },
{ "slug": "straight-drinks", "label": "Straight Drinks", "shortLabel": "Drinks", "order": 4 },
{ "slug": "delivery", "label": "Delivery", "shortLabel": "Delivery", "order": 5 },
{ "slug": "whats-on", "label": "What's On", "shortLabel": "What's On", "order": 6 }
{
"slug": "food",
"label": "Food",
"shortLabel": "Food",
"order": 1
},
{
"slug": "bar",
"label": "Bar",
"shortLabel": "Bar",
"order": 2
},
{
"slug": "wine",
"label": "Wine",
"shortLabel": "Wine",
"order": 3
},
{
"slug": "straight-drinks",
"label": "Straight Drinks",
"shortLabel": "Drinks",
"order": 4
},
{
"slug": "delivery",
"label": "Delivery",
"shortLabel": "Delivery",
"order": 5
},
{
"slug": "whats-on",
"label": "What's On",
"shortLabel": "What's On",
"order": 6
}
],
"limits": {
"maxUploadBytes": 31457280,
Binary file not shown.

After

Width:  |  Height:  |  Size: 716 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 628 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 206 KiB

@@ -0,0 +1,261 @@
---
name: Atelier Dining & Hospitality
colors:
surface: '#fbf9f6'
surface-dim: '#dbdad7'
surface-bright: '#fbf9f6'
surface-container-lowest: '#ffffff'
surface-container-low: '#f5f3f0'
surface-container: '#efeeeb'
surface-container-high: '#eae8e5'
surface-container-highest: '#e4e2df'
on-surface: '#1b1c1a'
on-surface-variant: '#4b4640'
inverse-surface: '#30312f'
inverse-on-surface: '#f2f0ed'
outline: '#7c766f'
outline-variant: '#cdc5bd'
surface-tint: '#605e5c'
primary: '#000000'
on-primary: '#ffffff'
primary-container: '#1d1b1a'
on-primary-container: '#868381'
inverse-primary: '#cac6c3'
secondary: '#7a581c'
on-secondary: '#ffffff'
secondary-container: '#ffd087'
on-secondary-container: '#79571b'
tertiary: '#000000'
on-tertiary: '#ffffff'
tertiary-container: '#291800'
on-tertiary-container: '#a67c3c'
error: '#ba1a1a'
on-error: '#ffffff'
error-container: '#ffdad6'
on-error-container: '#93000a'
primary-fixed: '#e6e1df'
primary-fixed-dim: '#cac6c3'
on-primary-fixed: '#1d1b1a'
on-primary-fixed-variant: '#484645'
secondary-fixed: '#ffdeae'
secondary-fixed-dim: '#edbf78'
on-secondary-fixed: '#281900'
on-secondary-fixed-variant: '#604103'
tertiary-fixed: '#ffddb1'
tertiary-fixed-dim: '#efbe77'
on-tertiary-fixed: '#291800'
on-tertiary-fixed-variant: '#614002'
background: '#fbf9f6'
on-background: '#1b1c1a'
surface-variant: '#e4e2df'
typography:
display-lg:
fontFamily: Playfair Display
fontSize: 56px
fontWeight: '400'
lineHeight: 64px
letterSpacing: -0.02em
display-lg-mobile:
fontFamily: Playfair Display
fontSize: 36px
fontWeight: '400'
lineHeight: 44px
letterSpacing: -0.01em
headline-xl:
fontFamily: Playfair Display
fontSize: 40px
fontWeight: '400'
lineHeight: 48px
letterSpacing: -0.01em
headline-xl-mobile:
fontFamily: Playfair Display
fontSize: 28px
fontWeight: '400'
lineHeight: 36px
letterSpacing: 0em
headline-lg:
fontFamily: Playfair Display
fontSize: 30px
fontWeight: '500'
lineHeight: 38px
headline-md:
fontFamily: Playfair Display
fontSize: 22px
fontWeight: '500'
lineHeight: 30px
headline-sm:
fontFamily: Plus Jakarta Sans
fontSize: 18px
fontWeight: '600'
lineHeight: 24px
body-lg:
fontFamily: Plus Jakarta Sans
fontSize: 16px
fontWeight: '400'
lineHeight: 26px
body-md:
fontFamily: Plus Jakarta Sans
fontSize: 14px
fontWeight: '400'
lineHeight: 22px
body-sm:
fontFamily: Plus Jakarta Sans
fontSize: 12px
fontWeight: '400'
lineHeight: 18px
label-lg:
fontFamily: Plus Jakarta Sans
fontSize: 13px
fontWeight: '600'
lineHeight: 18px
letterSpacing: 0.02em
label-md:
fontFamily: Plus Jakarta Sans
fontSize: 11px
fontWeight: '600'
lineHeight: 16px
letterSpacing: 0.06em
label-sm:
fontFamily: Plus Jakarta Sans
fontSize: 10px
fontWeight: '700'
lineHeight: 14px
letterSpacing: 0.08em
currency-md:
fontFamily: Plus Jakarta Sans
fontSize: 15px
fontWeight: '600'
lineHeight: 20px
letterSpacing: 0.01em
rounded:
sm: 0.5rem
DEFAULT: 1rem
md: 1.5rem
lg: 2rem
xl: 3rem
full: 9999px
spacing:
gutter: 1.5rem
gutter-sm: 1rem
margin: 2.5rem
margin-sm: 1.25rem
space-xs: 0.25rem
space-sm: 0.5rem
space-md: 1rem
space-lg: 1.5rem
space-xl: 2.5rem
---
## Brand & Style
This design system pairs the tactile grace of Michelin-caliber editorial print with the razor-sharp operational ergonomics required by high-velocity hospitality management. The aesthetic bridges two primary touchpoints: the guest-facing digital menu experience (epicurean, sensory, leisurely) and the back-of-house administrative command suite (lucid, dense, decisive).
The brand communicates restraint, discernment, and bespoke craft. It deliberately rejects noisy animations, saturated alert badges, and harsh utilitarian UI tropes in favor of warm, quiet authority. The visual language blends:
- **Warm Editorial Minimalism:** Generous negative space, museum-grade typographic hierarchy, and paper-textured tonal canvas shifts.
- **Modern Executive Precision:** Structured data grids, micro-etched dividers, disciplined layout modules, and subdued state transitions.
Target users span fine-dining proprietors, beverage directors, and floor captains managing inventory and covers in real time, alongside discerning diners experiencing a curated tableside or pre-arrival digital carte.
## Colors
The system establishes an evocative dual-surface architecture: a luminous, parchment-tinted guest presentation mode contrasted against an espresso-grounded administrative control environment.
- **Primary (`#121110`):** Deep charcoal espresso. Used for primary typography in public surfaces, dominant action containers, and primary surfaces in dark management viewports.
- **Secondary (`#C59B58`):** Warm burnished gold. Deployed deliberately for active states, vintage highlights, tasting notes, key curation tags, and focal callouts.
- **Tertiary (`#B08544`):** Deep aged amber. Serves as interactive hover values, tactile micro-accents, pressed states, and secondary visual balances.
- **Neutral Surface Baseline (`#FAF8F5`):** Warm ivory linen. Forms the underlying page canvas, evoking heavy unbleached cotton menu stock.
### Supporting Tone System
- **Sub-Surface Canvas:** `#F4F1EA` (warm alabaster cream for inset cards, table rows, and secondary wells).
- **Hairline Dividers & Framing:** `#E6E1D8` (muted bone linen with low visual vibration).
- **Dark Surface Modules:** `#1E1C1A` (warm obsidian for administrative pods, metric cards, and back-of-house viewports).
- **Dark Surface Hairlines:** `#2D2A26` (charred espresso borders for dark-mode cards and tables).
- **Semantic Signals:** Muted moss green (`#4A6B53`) for table ready/open items, subtle terracotta red (`#A34839`) for 86'd/unavailable items, and soft slate amber (`#D99E32`) for active reservations or pending kitchen courses.
## Typography
Typography governs the dual identity of this system. `Playfair Display` evokes the provenance of printed carte menus, vineyard reserves, and editorial headlining. It must remain untruncated and breathed with open line heights. It is reserved exclusively for major restaurant identity sections, dish names, tasting menu chapters, and prominent administrative dashboard greetings.
`Plus Jakarta Sans` handles all transactional clarity: ingredient pairings, culinary provenance descriptions, dietary identifiers, inventory counts, modifier groups, and data tables.
### Rules of Application
- All uppercase section labels and culinary course markers (`AMUSE-BOUCHE`, `PREMIER`, `CELLAR RESERVE`) must utilize `label-md` or `label-sm` with explicit letter-spacing between `0.06em` and `0.08em`.
- Pricing labels must never use serif fonts; they must render in `currency-md` or `label-lg` using tabular numbers (`font-variant-numeric: tabular-nums`) to align vertically across dish items and administrative balance sheets.
- Line heights on editorial dish descriptions remain generous (`body-md` at `22px`) to ensure optimal readability under dim, ambient dining room lighting conditions.
## Layout & Spacing
The system deploys an adaptable 12-column responsive grid with a dual rhythmic structure:
- **Desktop Operations Suite (>= 1200px):** 12-column structure with `2.5rem` (`margin`) outer offsets and `1.5rem` (`gutter`). Fixed sidebar navigation (width `260px` or collapsed to `72px`) paired with fluid administrative canvas modules.
- **Tablet Service View (768px - 1199px):** 8-column layout with `1.5rem` margins and `1rem` gutters optimized for floor management tablets, order dispatch stations, and wine list browsing.
- **Mobile Guest Menu (< 768px):** 4-column layout with `1.25rem` (`margin-sm`) canvas boundaries. Content stacks vertically with deliberate whitespace between course sections to preserve editorial luxury.
### Spacing Scale & Rhythm
- Spacing relies on a base 8px increment system supplemented by 4px micro-steps (`space-xs`).
- Section separations within digital menu books require intentional room: dishes within a single course sit apart by `space-md` (`1rem`), while distinct courses (`Entrées` to `Desserts`) transition across `space-xl` (`2.5rem`).
- Table layout densities in SaaS viewports default to comfortable padded lines (`0.75rem` vertical cell padding) with a dense toggle (`0.5rem`) for high-volume service shifts.
## Elevation & Depth
Spatial depth avoids synthetic heavy dropshadows in favor of tactile surfaces, whisper-thin borders, and warm ambient luminescence.
### 1. Ghost Border Layering
Surfaces default to structural containment through subtle lines rather than elevation. In light mode, containers leverage a 1px border of `#E6E1D8`. In dark operations mode, surfaces frame in 1px `#2D2A26`.
### 2. Warm Ambient Shadow (Level 1)
Applied to elevated menu cards, active category pills, and dropdown selectors:
- `box-shadow: 0 4px 20px -2px rgba(18, 17, 16, 0.04), 0 2px 6px -1px rgba(18, 17, 16, 0.02)`
### 3. Floated Surface & Overlay (Level 2)
Applied to tableside modifier trays, wine vintage filter sheets, and administrative modals:
- `box-shadow: 0 12px 36px -4px rgba(18, 17, 16, 0.08), 0 4px 12px -2px rgba(18, 17, 16, 0.04)`
- Paired with an editorial frosted overlay: `background: rgba(18, 17, 16, 0.45); backdrop-filter: blur(8px);`
### 4. Back-of-House Dark Elevation
For the administrative cards (`#1E1C1A`), stacked elements rely on tonal surface lightening (`#262320` for nested sub-cards or hover rows) rather than drop shadows, maintaining crisp contrast against the `#121110` workspace background.
## Shapes
The shape architecture centers on high-tactile refinement. Structural content carriers (menu containers, data panels, administrative tables, dish image wrappers) use deliberate micro-radii to maintain discipline and architectural structure, while interactive elements leverage full pill profiles (`roundedness: 3`) to evoke polished physical artifacts.
- **Primary & Secondary Action Controls:** Full pill geometry (`rounded-full` / 9999px) delivering soft, smooth touch targets suitable for finger tapping on floor devices or guest mobile screens.
- **Badges, Vintage Years, & Diet Tags:** Pill silhouettes (`rounded-full`) that cradle typography with proportional horizontal padding.
- **Card Containers & Modules:** Soft architectural radius (`0.5rem` to `0.75rem`) preventing the interface from feeling clinical or sharp.
- **Dish Photography & Imagery:** Cropped with subtle `0.5rem` corners or displayed as pristine full-bleed rectangles with hairline interior frames.
## Components
### Buttons
- **Primary Pill:** Background `#121110`, text `#FAF8F5`, hairline gold interior halo on hover (`box-shadow: inset 0 0 0 1px #C59B58`). Fully rounded pill shape.
- **Gold Accent Pill:** Background `#C59B58`, text `#121110`, transitioning to `#B08544` on active press. Reserved for high-value conversions ("Reserve Table", "Order Tasting Pairings", "Confirm Service").
- **Secondary Ghost Pill:** Background transparent, border 1px solid `#E6E1D8`, text `#121110`. Hover invokes background `#F4F1EA`.
- **Admin Mini Actions:** Subdued dark pills (`#1E1C1A` with border 1px solid `#2D2A26`, text `#FAF8F5`).
### Dish & Product Cards
- **Editorial Menu Row:** Borderless or separated by hairline `#E6E1D8`. Features the dish title in `headline-md` (`Playfair Display`), dot leader or right-aligned price in `currency-md`, followed by component descriptions in `body-md` (`Plus Jakarta Sans`).
- **Showcase Dish Card:** Clean parchment surface (`#FAF8F5`), 1px `#E6E1D8` border, subtle 16:10 ratio photograph with soft radius, dietary pill badges pinned at bottom-left, price anchored at top-right.
### Chips & Dietary Badges
- Compact pill geometries with heights of 24px to 28px.
- **Dietary Tags (GF, V, VG, NF):** Background `#F4F1EA`, border 1px solid `#E6E1D8`, text `#121110`, uppercase `label-sm`.
- **Sommelier Selection / Reserve Pick:** Background `rgba(197, 155, 88, 0.12)`, border 1px solid `#C59B58`, text `#B08544`.
### Input Fields & Selectors
- Background `#FFFFFF` (guest views) or `#1E1C1A` (admin views).
- Border 1px solid `#E6E1D8` (admin: `#2D2A26`).
- Height 44px for high touch accessibility.
- Focus state: Border transitions to `#C59B58` with no loud glow rings; utilizes a faint 2px ambient amber whisper ring (`rgba(197, 155, 88, 0.18)`).
- Typography: `body-md` with placeholder text in warm stone tone (`#8C857B`).
### Checkboxes, Radios, & Switches
- **Radios & Checkboxes:** Unchecked state defined by 1.5px border `#E6E1D8`. Checked state fills `#121110` with gold accent tick mark (`#C59B58`).
- **Hospitality Toggle Switch:** 20px height pill track; inactive track `#E6E1D8`, active track `#121110`. The toggle knob is cream `#FAF8F5`, sliding smoothly with a warm micro-shadow.
### Admin Data Tables & Kitchen Station Queues
- **Header:** Sticky top, uppercase `label-md` in stone charcoal (`#59544D`), background `#FAF8F5` or dark `#121110`.
- **Row Architecture:** Subtle bottom divider `#E6E1D8` (dark: `#2D2A26`). Alternating or hover tint using `#F4F1EA` (dark: `#262320`).
- **Live Status Indicators:** 6px solid circular pips:
- *Seated / Active:* `#4A6B53` (Moss)
- *86'd / Depleted:* `#A34839` (Terracotta)
- *Reserved / Firing:* `#D99E32` (Gold)
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 MiB

+1 -1
View File
@@ -13,7 +13,7 @@ For whoever is on the other end of "the menu is wrong". The design is in
| Admin | `https://<vhost>/admin` |
| The password | `/var/lib/menu-app.<site>/site.env` (mode 600, never in git) |
| Categories | `data/sections.json`, edited in the admin |
| Brand and theme | `config/<site>.site.json` in this repo, baked into the image |
| Brand and theme | `config/<site>.site.json` in this repo, baked into the image (the palette follows the Stitch system in `docs/design/stitch/`) |
## Everyday tasks
+2 -2
View File
@@ -89,7 +89,7 @@ Protection: a `scrypt` hash (from `node:crypto`, so no native build dependency)
### Customer design
The navigation is made of the restaurant's own paper: the landing is a row of cards, each one the first page of a menu with the category name over it and the page count on a strip beneath, scrolling sideways and snapping (wrapping into a grid on a wide screen). The bar links every menu as a filled segment, the current one the only teal thing on screen, and a 2px brass line under it tracks how far through the pages you are.
The look is the Atelier Dining & Hospitality system from Stitch (`docs/design/stitch/`): warm ivory linen and espresso for the guest surfaces, burnished gold for the one active thing, Playfair Display carrying the restaurant's voice, Plus Jakarta Sans carrying everything transactional. The landing is an editorial directory — each category a hairline row with a small plate of that menu's paper, its name in Playfair, a dot leader and a tabular page count. The bar names the restaurant, links every menu as a pill (the current one espresso-filled), and a gold hairline under it tracks progress through the pages. The administrative side is the same system in its dark mode: espresso workspace, obsidian pods, gold actions.
```
┌────────────────────────────┐ ┌────────────────────────────┐
@@ -114,7 +114,7 @@ The navigation is made of the restaurant's own paper: the landing is a row of ca
- New repo `menu-admin` on `git.tanshu.com`: app source, `Dockerfile`, `compose.yaml` for local dev, `docs/` (this plan, `CONTEXT.md`, the ADRs), `node --test` tests, and `deploy.sh` (tests, build, push).
- The container is `node:24-alpine` running TypeScript directly, so there is no build step; sharp's musl binaries come from `npm ci` on the image's own platform. It writes only under `MENU_ROOT` and healthchecks its own `/healthz`.
- The Site's brand and theme live in `config/<site>.site.json`, baked into the image and selected with `MENU_SITE_CONFIG`; its Section list only seeds `data/sections.json` the first time. Categories are edited in the admin from then on (ADR 0007), so adding one does not need a redeploy.
- The design system is documented in `docs/design/stitch/atelier-dining-hospitality.md`, with the four screens Stitch produced beside it. The Site's brand and theme live in `config/<site>.site.json`, baked into the image and selected with `MENU_SITE_CONFIG`; its Section list only seeds `data/sections.json` the first time. Categories are edited in the admin from then on (ADR 0007), so adding one does not need a redeploy.
- Build locally and push `registry.tanshu.com/menu-admin:<tag>`, matching the sibling `deploy.sh` / `make build-production TAG=` convention.
- New `menu-admin` playbook in `ansible-playbooks` with per-Site vars (`vars/hopsngrains.yml` first, the other two stubbed): roles `network`, `menu-app` (pull image, create `/var/lib/menu-app.<site>`, start container, health check, then `verify` the data directory) and `caddy` (snippet upsert).
- The existing `roles/caddy` only *adds* a snippet when its exact text is missing, which cannot change a proxy target; the new playbook's role uses a stable marker so `blockinfile` updates in place, and removes the old role's per-hostname marker at cutover so Caddy never holds two blocks for one hostname.
+59
View File
@@ -1,3 +1,62 @@
/* The administrative side of the same system: an espresso workspace with warm
obsidian pods and burnished gold for the one active thing. See
docs/design/stitch/atelier-dining-hospitality.md. */
body.admin-page {
--steel: #121110;
--rail: #1e1c1a;
--paper: #faf8f5;
--ink: #faf8f5;
--ink-soft: #c9c4bd;
--ink-dim: #a9a29a;
--ink-on-paper: #121110;
--accent: #c59b58;
--copper: #b08544;
--hairline: #2d2a26;
--hairline-soft: #262320;
--sheet-shadow: rgba(0, 0, 0, 0.45);
--shadow-card: none;
--shadow-float: 0 12px 36px -4px rgba(0, 0, 0, 0.5);
--radius-card: 0.5rem;
--radius-pill: 9999px;
}
body {
font-family: var(--font-ui, 'Plus Jakarta Sans', system-ui, sans-serif);
}
.button {
border-radius: var(--radius-pill);
font-size: 0.78rem;
font-weight: 600;
letter-spacing: 0.02em;
}
.field input,
.catrow__name input {
min-height: 44px;
border-radius: var(--radius-card);
}
.field input:focus,
.catrow__name input:focus {
border-color: var(--accent);
box-shadow: 0 0 0 2px rgba(197, 155, 88, 0.18);
outline: none;
}
.page-admin,
.drop {
border-radius: var(--radius-card);
}
.pill {
border-radius: var(--radius-pill);
letter-spacing: 0.06em;
text-transform: uppercase;
font-size: 0.62rem;
}
/* The admin shares the customer tokens and stays out of the way: forms,
lists, and thumbnails good enough to judge a page by. */
+93
View File
@@ -0,0 +1,93 @@
Copyright 2017 The Playfair Display Project Authors (https://github.com/clauseggers/Playfair-Display), with Reserved Font Name "Playfair Display"
This Font Software is licensed under the SIL Open Font License, Version 1.1.
This license is copied below, and is also available with a FAQ at:
http://scripts.sil.org/OFL
-----------------------------------------------------------
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
-----------------------------------------------------------
PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font creation
efforts of academic and linguistic communities, and to provide a free and
open framework in which fonts may be shared and improved in partnership
with others.
The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded,
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply
to any document created using the fonts or their derivatives.
DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.
"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).
"Original Version" refers to the collection of Font Software components as
distributed by the Copyright Holder(s).
"Modified Version" refers to any derivative made by adding to, deleting,
or substituting -- in part or in whole -- any of the components of the
Original Version, by changing formats or by porting the Font Software to a
new environment.
"Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.
PERMISSION & CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed, modify,
redistribute, and sell modified and unmodified copies of the Font
Software, subject to the following conditions:
1) Neither the Font Software nor any of its individual components,
in Original or Modified Versions, may be sold by itself.
2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.
3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the corresponding
Copyright Holder. This restriction only applies to the primary font name as
presented to the users.
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.
5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created
using the Font Software.
TERMINATION
This license becomes null and void if any of the above conditions are
not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.
@@ -1,4 +1,4 @@
Copyright 2020 The Archivo Project Authors (https://github.com/Omnibus-Type/Archivo)
Copyright 2020 The Plus Jakarta Sans Project Authors (https://github.com/tokotype/PlusJakartaSans)
This Font Software is licensed under the SIL Open Font License, Version 1.1.
This license is copied below, and is also available with a FAQ at:
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+218 -175
View File
@@ -1,49 +1,54 @@
/* Hops n Grains menus.
The chrome is the shelf; the menus are the paper standing on it. Brass is the
taproom's metal, the logo's teal is the only active colour, and the paper of
the photographed menus is the brightest thing on the page. */
/* Hops n Grains menus, to the Atelier Dining & Hospitality system that came out
of Stitch: ivory linen and espresso for the guest's menu, burnished gold for
the one active thing on screen, Playfair Display carrying the restaurant's
voice and Plus Jakarta Sans carrying everything transactional.
See docs/design/stitch/atelier-dining-hospitality.md. */
@font-face {
font-family: 'Archivo';
font-family: 'Playfair Display';
font-style: normal;
font-weight: 100 900;
font-stretch: 62% 125%;
font-weight: 400 700;
font-display: swap;
src: url('/assets/fonts/archivo-latin.woff2') format('woff2');
src: url('/assets/fonts/playfair-display-latin.woff2') format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304,
U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
font-family: 'Archivo';
font-family: 'Plus Jakarta Sans';
font-style: normal;
font-weight: 100 900;
font-stretch: 62% 125%;
font-weight: 200 800;
font-display: swap;
src: url('/assets/fonts/archivo-latin-ext.woff2') format('woff2');
unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308,
U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113,
U+2C60-2C7F, U+A720-A7FF;
src: url('/assets/fonts/plus-jakarta-sans-latin.woff2') format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304,
U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
:root {
--steel: #101719;
--rail: #182125;
--paper: #efeae0;
--accent: #0088aa;
--copper: #c08544;
--ink: #e8e5dd;
--ink-dim: #8f979a;
--ink-on-paper: #1b1f22;
--brass-edge: color-mix(in srgb, var(--copper) 42%, transparent);
--hairline: color-mix(in srgb, var(--copper) 26%, transparent);
--hairline-soft: color-mix(in srgb, var(--copper) 14%, transparent);
--sheet-shadow: color-mix(in srgb, #000 45%, transparent);
/* guest surfaces: warm ivory linen, never stark white */
--steel: #faf8f5;
--rail: #f4f1ea;
--paper: #ffffff;
--ink: #121110;
--ink-soft: #4b4640;
--ink-dim: #8c857b;
--ink-on-paper: #121110;
--accent: #c59b58;
--copper: #b08544;
--hairline: #e6e1d8;
--hairline-soft: #efebe4;
--sheet-shadow: rgba(18, 17, 16, 0.06);
--shadow-card: 0 4px 20px -2px rgba(18, 17, 16, 0.04), 0 2px 6px -1px rgba(18, 17, 16, 0.02);
--shadow-float: 0 12px 36px -4px rgba(18, 17, 16, 0.08), 0 4px 12px -2px rgba(18, 17, 16, 0.04);
--font-display: 'Playfair Display', Georgia, 'Times New Roman', serif;
--font-ui: 'Plus Jakarta Sans', system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif;
--radius-card: 0.5rem;
--radius-pill: 9999px;
--step-0: 1rem;
--step-1: 1.25rem;
--step-2: 1.55rem;
--step-2: 1.75rem;
--page-max: 780px;
--content-max: 52rem;
--content-max: 46rem;
}
*,
@@ -60,7 +65,7 @@ body {
margin: 0;
background: var(--steel);
color: var(--ink);
font-family: 'Archivo', system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif;
font-family: var(--font-ui);
font-size: var(--step-0);
line-height: 1.5;
-webkit-font-smoothing: antialiased;
@@ -76,70 +81,62 @@ a {
outline-offset: 3px;
}
/* ---------- Landing: the menus as tap handles ----------
Each category is a card whose face is the first page of that menu, so the
navigation is made of the restaurant's own paper. The row scrolls sideways
and snaps; on a wide screen it wraps into a grid instead. */
/* Every uppercase label in this system is tracked, never tight. */
.label {
font-size: 0.68rem;
font-weight: 600;
letter-spacing: 0.08em;
text-transform: uppercase;
}
/* ---------- Landing: the menu directory ---------- */
.landing {
max-width: var(--content-max);
margin: 0 auto;
padding: 0 0 3rem;
padding: 0 1.25rem 3.5rem;
}
.masthead {
padding: clamp(2.2rem, 10vh, 4.5rem) 1.1rem 1.5rem;
padding: clamp(2.25rem, 10vh, 4.5rem) 0 1.75rem;
border-bottom: 1px solid var(--hairline);
}
.wordmark {
margin: 0;
font-size: clamp(1.95rem, 8.5vw, 2.9rem);
font-weight: 700;
font-stretch: 118%;
letter-spacing: -0.025em;
line-height: 1;
font-family: var(--font-display);
font-size: clamp(2rem, 8vw, 2.75rem);
font-weight: 400;
letter-spacing: -0.02em;
line-height: 1.05;
}
.masthead__tagline {
margin: 0.5rem 0 0;
color: var(--ink-dim);
font-size: 0.98rem;
margin: 0.6rem 0 0;
color: var(--ink-soft);
font-size: 0.95rem;
}
.shelves {
display: flex;
gap: 0.85rem;
padding: 0.15rem 0 1rem 1.1rem;
overflow-x: auto;
scroll-snap-type: x mandatory;
/* snap against the same inset the first card starts at, so nothing is ever
left half-scrolled under the screen edge */
scroll-padding-left: 1.1rem;
scrollbar-width: none;
-webkit-overflow-scrolling: touch;
}
.shelves::-webkit-scrollbar {
display: none;
display: block;
}
.shelf {
position: relative;
flex: 0 0 10.5rem;
height: 15rem;
overflow: hidden;
background: var(--paper);
box-shadow: 0 1px 0 var(--sheet-shadow);
scroll-snap-align: start;
display: flex;
align-items: center;
gap: 0.9rem;
padding: 0.95rem 0;
border-bottom: 1px solid var(--hairline);
}
.shelf__peek {
position: absolute;
top: 0;
right: 0;
bottom: 3.2rem;
left: 0;
display: block;
flex: 0 0 auto;
width: 3.4rem;
height: 4.4rem;
overflow: hidden;
border: 1px solid var(--hairline);
border-radius: var(--radius-card);
background: var(--paper);
}
.shelf__peek img {
@@ -152,49 +149,48 @@ a {
}
.shelf__name {
position: absolute;
right: 0;
bottom: 3.2rem;
left: 0;
padding: 0.45rem 0.6rem;
background: color-mix(in srgb, var(--steel) 88%, transparent);
font-size: 1.05rem;
font-weight: 700;
font-family: var(--font-display);
font-size: 1.45rem;
font-weight: 400;
letter-spacing: -0.01em;
}
.shelf__pages {
position: absolute;
right: 0;
bottom: 0;
left: 0;
height: 3.2rem;
padding: 0.85rem 0.6rem;
background: var(--paper);
color: var(--ink-on-paper);
font-size: 0.8rem;
font-variant-numeric: tabular-nums;
line-height: 1.15;
}
.shelf:hover .shelf__name,
.shelf:focus-visible .shelf__name {
background: color-mix(in srgb, var(--accent) 90%, transparent);
color: var(--steel);
color: var(--amber-deep, #b08544);
}
.shelf:hover .shelf__peek img,
.shelf:focus-visible .shelf__peek img {
transform: scale(1.04);
transform: scale(1.05);
}
/* the dot leader that carries the eye from the name to the count */
.leader {
flex: 1 1 auto;
align-self: flex-end;
margin-bottom: 0.4rem;
border-bottom: 1px dotted #cdc5bd;
}
.shelf__pages {
flex: 0 0 auto;
color: var(--ink-soft);
font-size: 0.78rem;
font-weight: 600;
font-variant-numeric: tabular-nums;
letter-spacing: 0.02em;
}
.empty {
max-width: 34ch;
margin: 1rem 1.1rem 0;
color: var(--ink-dim);
font-size: var(--step-1);
margin: 1.25rem 0 0;
color: var(--ink-soft);
font-size: 1.05rem;
}
/* ---------- Section page: the bar and the switcher ---------- */
/* ---------- Section page: the menu bar ---------- */
.bar {
position: sticky;
@@ -202,27 +198,38 @@ a {
z-index: 20;
display: flex;
align-items: center;
gap: 0.7rem;
padding: 0.5rem 0.85rem 0.45rem;
background: color-mix(in srgb, var(--rail) 94%, transparent);
backdrop-filter: blur(6px);
gap: 1rem;
padding: 0.6rem 1.25rem;
background: color-mix(in srgb, var(--steel) 92%, transparent);
backdrop-filter: blur(8px);
border-bottom: 1px solid var(--hairline);
}
.bar__brand {
display: flex;
align-items: center;
gap: 0.55rem;
flex: 0 0 auto;
}
.bar__brand img {
display: block;
width: 24px;
height: 24px;
width: 26px;
height: 26px;
border-radius: var(--radius-pill);
}
.bar__name {
font-family: var(--font-display);
font-size: 1.05rem;
letter-spacing: -0.01em;
white-space: nowrap;
}
.bar__nav {
display: flex;
flex: 1 1 auto;
gap: 0.15rem;
gap: 0.35rem;
overflow-x: auto;
scrollbar-width: none;
-webkit-overflow-scrolling: touch;
@@ -233,38 +240,41 @@ a {
}
.bar__link {
padding: 0.35rem 0.65rem;
background: color-mix(in srgb, var(--ink) 9%, transparent);
color: var(--ink-dim);
font-size: 0.95rem;
padding: 0.42rem 0.9rem;
border: 1px solid transparent;
border-radius: var(--radius-pill);
color: var(--ink-soft);
font-size: 0.78rem;
font-weight: 600;
letter-spacing: 0.02em;
white-space: nowrap;
transition: background 0.18s ease, color 0.18s ease;
}
.bar__link:hover {
background: color-mix(in srgb, var(--accent) 16%, transparent);
color: var(--ink);
background: color-mix(in srgb, var(--ink) 15%, transparent);
}
.bar__link.is-current {
background: var(--accent);
background: var(--ink);
color: var(--steel);
font-weight: 700;
}
.pour {
position: absolute;
right: 0;
bottom: 0;
bottom: -1px;
left: 0;
height: 2px;
background: var(--hairline-soft);
background: transparent;
}
.pour__fill {
display: block;
width: 0;
height: 100%;
background: var(--copper);
background: var(--accent);
}
/* ---------- The pages themselves ---------- */
@@ -275,52 +285,58 @@ a {
justify-content: space-between;
gap: 1rem;
max-width: var(--page-max);
margin: 1.2rem auto 0.75rem;
padding: 0 0.9rem;
margin: 1.75rem auto 1rem;
padding: 0 1.25rem;
}
.section-heading__label {
font-family: var(--font-display);
font-size: var(--step-2);
font-weight: 700;
letter-spacing: -0.02em;
font-weight: 400;
letter-spacing: -0.015em;
}
.section-heading__date {
color: var(--ink-dim);
font-size: 0.88rem;
font-size: 0.68rem;
font-weight: 600;
letter-spacing: 0.08em;
text-transform: uppercase;
font-variant-numeric: tabular-nums;
}
.notice {
max-width: var(--page-max);
margin: 0 auto 1rem;
padding: 0.6rem 0.8rem;
border-left: 2px solid var(--copper);
margin: 0 auto 1.25rem;
padding: 0.7rem 0.9rem;
border-left: 2px solid var(--accent);
background: var(--rail);
font-size: 0.95rem;
font-size: 0.9rem;
}
.notice a {
color: var(--accent);
color: var(--copper);
text-decoration: underline;
}
.notice--admin {
max-width: none;
border-left-color: var(--accent);
background: color-mix(in srgb, var(--accent) 20%, var(--rail));
background: color-mix(in srgb, var(--accent) 12%, var(--rail));
}
.pages {
max-width: var(--page-max);
margin: 0 auto;
padding: 0 0.7rem 1.25rem;
padding: 0 1.25rem 1.5rem;
}
.page {
margin: 0 0 0.6rem;
margin: 0 0 0.85rem;
padding: 0.4rem;
background: var(--paper);
box-shadow: 0 1px 0 color-mix(in srgb, #000 35%, transparent);
border: 1px solid var(--hairline);
border-radius: var(--radius-card);
box-shadow: var(--shadow-card);
}
.page__zoom {
@@ -331,25 +347,29 @@ a {
display: block;
width: 100%;
height: auto;
border-radius: 0.25rem;
}
/* ---------- Footer: page index and older menus ---------- */
.foot {
max-width: var(--page-max);
margin: 0 auto 4.5rem;
padding: 0 0.9rem 1rem;
border-top: 1px solid var(--hairline-soft);
margin: 0 auto 5rem;
padding: 0 1.25rem 1rem;
border-top: 1px solid var(--hairline);
}
.fold {
padding: 0.7rem 0;
border-bottom: 1px solid var(--hairline-soft);
padding: 0.85rem 0;
border-bottom: 1px solid var(--hairline);
}
.fold summary {
color: var(--ink-dim);
font-size: 0.92rem;
color: var(--ink-soft);
font-size: 0.68rem;
font-weight: 600;
letter-spacing: 0.08em;
text-transform: uppercase;
cursor: pointer;
}
@@ -360,29 +380,33 @@ a {
.index {
display: flex;
flex-wrap: wrap;
gap: 0.4rem;
gap: 0.45rem;
margin: 0;
padding: 0.85rem 0 0;
padding: 1rem 0 0;
list-style: none;
}
.index a {
display: block;
min-width: 2.3rem;
padding: 0.3rem 0.45rem;
background: var(--rail);
min-width: 2.4rem;
padding: 0.35rem 0.5rem;
border: 1px solid var(--hairline);
border-radius: var(--radius-pill);
color: var(--ink-soft);
font-size: 0.78rem;
font-weight: 600;
font-variant-numeric: tabular-nums;
text-align: center;
}
.index a:hover,
.index a:focus-visible {
background: var(--accent);
background: var(--ink);
color: var(--steel);
}
.older {
margin: 0.5rem 0 0;
margin: 0.6rem 0 0;
padding: 0;
list-style: none;
}
@@ -391,25 +415,30 @@ a {
display: flex;
justify-content: space-between;
gap: 1rem;
padding: 0.45rem 0.15rem;
padding: 0.55rem 0.15rem;
border-bottom: 1px solid var(--hairline-soft);
}
.older__label {
font-weight: 500;
font-family: var(--font-display);
font-size: 1.05rem;
}
.older__date {
color: var(--ink-dim);
font-size: 0.88rem;
font-size: 0.72rem;
font-weight: 600;
letter-spacing: 0.06em;
text-transform: uppercase;
font-variant-numeric: tabular-nums;
}
.fold__back {
margin: 0.7rem 0 0;
margin: 0.85rem 0 0;
}
.fold__back a {
color: var(--accent);
color: var(--copper);
text-decoration: underline;
}
@@ -417,19 +446,24 @@ a {
.counter {
position: fixed;
right: 0.85rem;
bottom: calc(0.85rem + env(safe-area-inset-bottom));
right: 1.25rem;
bottom: calc(1.25rem + env(safe-area-inset-bottom));
z-index: 30;
display: inline-flex;
align-items: baseline;
gap: 0.3rem;
padding: 0.45rem 0.7rem;
border: 1px solid var(--brass-edge);
background: var(--paper);
color: var(--ink-on-paper);
gap: 0.35rem;
padding: 0.55rem 0.9rem;
border: 0;
border-radius: var(--radius-pill);
background: var(--ink);
color: var(--steel);
font: inherit;
font-size: 0.88rem;
font-size: 0.72rem;
font-weight: 600;
letter-spacing: 0.08em;
text-transform: uppercase;
font-variant-numeric: tabular-nums;
box-shadow: var(--shadow-float);
cursor: pointer;
}
@@ -438,7 +472,7 @@ a {
}
.counter__total {
color: color-mix(in srgb, var(--ink-on-paper) 55%, transparent);
color: color-mix(in srgb, var(--steel) 62%, transparent);
}
.zoom {
@@ -446,8 +480,10 @@ a {
inset: 0;
z-index: 50;
overflow: auto;
background: rgba(6, 9, 10, 0.96);
background: rgba(18, 17, 16, 0.45);
backdrop-filter: blur(8px);
-webkit-overflow-scrolling: touch;
padding: 1.25rem 0;
}
.zoom img {
@@ -455,34 +491,41 @@ a {
width: min(100%, 1600px);
height: auto;
margin: 0 auto;
border-radius: var(--radius-card);
box-shadow: var(--shadow-float);
}
.zoom__close {
position: fixed;
top: 0.6rem;
right: 0.6rem;
top: 0.75rem;
right: 0.75rem;
z-index: 1;
padding: 0.5rem 0.75rem;
border: 1px solid var(--brass-edge);
background: var(--paper);
color: var(--ink-on-paper);
padding: 0.55rem 1rem;
border: 0;
border-radius: var(--radius-pill);
background: var(--ink);
color: var(--steel);
font: inherit;
font-size: 0.72rem;
font-weight: 600;
letter-spacing: 0.08em;
text-transform: uppercase;
cursor: pointer;
}
/* ---------- Wider screens ---------- */
@media (min-width: 62rem) {
.shelves {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(10.5rem, 1fr));
gap: 1rem;
padding: 0.15rem 1.1rem 1rem;
overflow: visible;
.landing {
padding-bottom: 4.5rem;
}
.shelf {
height: 16rem;
padding: 1.15rem 0;
}
.shelf__name {
font-size: 1.7rem;
}
.pages {
@@ -492,7 +535,6 @@ a {
@media print {
.bar,
.sheet,
.counter,
.fold,
.notice {
@@ -501,10 +543,11 @@ a {
body {
background: #fff;
color: #000;
}
.page {
border: 0;
box-shadow: none;
break-inside: avoid;
}
}
+3 -1
View File
@@ -72,7 +72,8 @@ export function setDateLabel(manifest: SetManifest): string {
function renderBar(config: SiteConfig, currentSlug: string, menu: readonly MenuLink[]): Html {
return html`<header class="bar">
<a class="bar__brand" href="/" aria-label="${config.brand.name} — all menus">
<img src="${config.brand.logo ?? '/assets/favicon.svg'}" alt="" width="24" height="24" />
<img src="${config.brand.logo ?? '/assets/favicon.svg'}" alt="" width="26" height="26" />
<span class="bar__name">${config.brand.name}</span>
</a>
<nav class="bar__nav" aria-label="Menus">
${join(
@@ -178,6 +179,7 @@ export function renderLanding(input: LandingInput): Html {
><img src="${entry.cover}" alt="" loading="lazy" decoding="async"
/></span>`}
<span class="shelf__name">${entry.section.label}</span>
<span class="leader" aria-hidden="true"></span>
<span class="shelf__pages">${pageCountLabel(entry.pages)}</span>
</a>`,
),