A design pass: the type ladder from the logo, and one name per thing

TYPOGRAPHY, taken from the lockup and walked down to something you can read a paragraph in. "The Vine" in
LeJour Script over COFFEEHOUSE + BAKERY in AdBhashitha is the whole brand voice, so: the script stays in
the wordmark and nowhere else (it is unreadable at length); AdBhashitha carries h1–h4 and the price, and
keeps the wordmark's 0.01em letter-spacing so a heading sits on the same rhythm as the logo above it;
`eyebrow` and `label` are the hinge — sans, but letterspaced like the tagline — and then plain Raleway.
Sizes are declared once in type.css instead of being re-chosen per template, and none of the type classes
set a colour, because the same heading appears in sage on cream and in cream on sage.

BUTTONS AND SURFACES have names now. `panel` is the white box that was "bg-white rounded-3xl shadow-xs"
written out in nine places (which is how two of them end up with different corners); `panel-lift` is one
that reacts. `pill` is one shape in four colourways, which exist because this site puts buttons on cream
AND on sage and they cannot be the same colour on both. `chip`/`chip-on` are the products filter.

RHYTHM: `section` (py-16 md:py-24) and `band` (py-14 md:py-20, tighter because a coloured strip is already
doing the separating) replace nine hand-picked paddings.

FOCUS: one visible ring for everything reachable by keyboard, as an outline rather than a shadow so it
works on cream, on white and on the sage bands, at zero specificity so a utility can still win.

Z-INDEX: written down in base.css, all two of it — 40 the sticky header, 30 the mobile panel, which is a
child of the header and so can only ever sit under the bar. Everything else stacks in document order. The
point of the comment is that a third layer doesn't get invented at 9999.

DROPDOWNS: the admin's category selects were drawing the platform's own arrow in the platform's grey; they
get the site's chevron and room for it.

53 tests green. Every class used across all nine pages — five public, four admin — resolves in the
compiled stylesheet, so nothing here is styled by a name that doesn't exist.
This commit is contained in:
2026-07-26 18:10:47 -05:00
parent 5225312a86
commit ec61b40b12
14 changed files with 266 additions and 76 deletions
+23
View File
@@ -18,6 +18,29 @@ there is no JavaScript framework in this repo. The look has not changed through
| Photos | public MinIO bucket `itsthevine`**not** in the repo or the image | | Photos | public MinIO bucket `itsthevine`**not** in the repo or the image |
| Deploy | Gitea CI → image → Watchtower → Caddy | | Deploy | Gitea CI → image → Watchtower → Caddy |
### The look, in one place
`src/main/styles` is the whole of it — no bundler, no framework, one stylesheet:
| file | what it holds |
|---|---|
| `theme.css` | the three typefaces and the sage-and-cream palette. Names, nothing drawn |
| `base.css` | bare elements: page background, body type, the fade-in, the focus ring, the z-index scale |
| `type.css` | the typographic ladder (below) |
| `components.css` | `panel`, `pill-*`, `chip`, `section`/`band` — the site's own classes |
| `admin.css` | the admin's controls, as `@utility` so `file:` variants work on the photo pickers |
**The type ladder comes out of the logo.** The lockup is "The Vine" in LeJour Script over COFFEEHOUSE +
BAKERY in AdBhashitha, letterspaced — and everything below is that idea made progressively more readable:
the script stays in the wordmark and nowhere else; AdBhashitha carries `h1``h4` and `price`, keeping the
wordmark's 0.01em letter-spacing so a heading sits on the same rhythm as the logo above it; `eyebrow` and
`label` are the hinge, sans but letterspaced like the tagline; then plain Raleway for anything you have to
read a paragraph of. None of the type classes set a colour — the same heading appears in sage on cream and
cream on sage.
Write those class names in templates rather than the utilities behind them: a button is `pill-sage`, and
the four colourways exist because the site puts buttons on cream *and* on sage.
### Why server-rendered ### Why server-rendered
The pages are content: a menu, a story, opening hours, a price list. Rendering them in the browser meant The pages are content: a menu, a story, opening hours, a price list. Rendering them in the browser meant
+15 -17
View File
@@ -8,7 +8,7 @@
that is selling something, and the same treatment as the homepage hero — blurred photo, sage wash — that is selling something, and the same treatment as the homepage hero — blurred photo, sage wash —
carries the brand rather than inventing a new look for it. carries the brand rather than inventing a new look for it.
*/--> */-->
<section class="relative flex items-center py-14 md:py-20 bg-bakery-900 text-white overflow-hidden"> <section class="band relative flex items-center bg-bakery-900 text-white overflow-hidden">
<img th:src="${photos.of('gallery/Cakes.webp')}" alt="" <img th:src="${photos.of('gallery/Cakes.webp')}" alt=""
class="absolute inset-0 w-full h-full object-cover blur-[3px] scale-110"> class="absolute inset-0 w-full h-full object-cover blur-[3px] scale-110">
<div class="absolute inset-0 bg-bakery-900/80"></div> <div class="absolute inset-0 bg-bakery-900/80"></div>
@@ -16,9 +16,9 @@
<div class="relative container mx-auto px-4 w-full min-w-0"> <div class="relative container mx-auto px-4 w-full min-w-0">
<div class="max-w-3xl mx-auto text-center"> <div class="max-w-3xl mx-auto text-center">
<p class="text-xs sm:text-sm uppercase tracking-[0.2em] text-bakery-200">Order ahead</p> <p class="eyebrow text-bakery-200">Order ahead</p>
<h1 class="mt-3 font-adbhashitha text-4xl md:text-5xl text-bakery-50">Goodie boxes &amp; catering</h1> <h1 class="h1 mt-3 text-bakery-50">Goodie boxes &amp; catering</h1>
<p class="mt-5 text-base sm:text-lg text-bakery-100 text-balance leading-relaxed"> <p class="lede mt-5 text-bakery-100">
Boxes for the office, packages for a party, and cakes and desserts for a wedding. Boxes for the office, packages for a party, and cakes and desserts for a wedding.
</p> </p>
</div> </div>
@@ -55,7 +55,7 @@
*/--> */-->
<section th:each="table : ${menu.packages}"> <section th:each="table : ${menu.packages}">
<header class="max-w-2xl mx-auto text-center"> <header class="max-w-2xl mx-auto text-center">
<h2 class="font-adbhashitha text-3xl md:text-4xl text-bakery-900" th:text="${table.name}">Office boxes</h2> <h2 class="h2 text-bakery-900" th:text="${table.name}">Office boxes</h2>
<p th:if="${table.blurb}" class="mt-3 text-bakery-700 leading-relaxed" th:text="${table.blurb}"> <p th:if="${table.blurb}" class="mt-3 text-bakery-700 leading-relaxed" th:text="${table.blurb}">
Mini pastries for a morning meeting. Mini pastries for a morning meeting.
</p> </p>
@@ -67,12 +67,11 @@
: (${#lists.size(table.tiers) == 2} ? 'sm:grid-cols-2 max-w-3xl mx-auto' : (${#lists.size(table.tiers) == 2} ? 'sm:grid-cols-2 max-w-3xl mx-auto'
: 'sm:grid-cols-2 lg:grid-cols-3')"> : 'sm:grid-cols-2 lg:grid-cols-3')">
<article th:each="tier, t : ${table.tiers}" <article th:each="tier, t : ${table.tiers}"
class="flex flex-col bg-white rounded-3xl shadow-xs p-6 md:p-8 class="flex flex-col panel-lift p-6 md:p-8">
transition-shadow duration-300 hover:shadow-lg"> <p class="label text-bakery-600" th:text="${tier.label}">Small</p>
<p class="text-xs uppercase tracking-[0.15em] text-bakery-600" th:text="${tier.label}">Small</p> <p th:if="${tier.price}" class="price mt-2 text-bakery-900" th:text="${tier.price}">$24</p>
<p th:if="${tier.price}" class="mt-2 font-adbhashitha text-4xl text-bakery-900" th:text="${tier.price}">$24</p>
<!--/* No price means "ask us" — say so rather than leaving a hole in the card. */--> <!--/* No price means "ask us" — say so rather than leaving a hole in the card. */-->
<p th:unless="${tier.price}" class="mt-2 font-adbhashitha text-2xl text-bakery-700">Ask us</p> <p th:unless="${tier.price}" class="price mt-2 text-2xl text-bakery-700">Ask us</p>
<!--/* "Small" doesn't tell anyone how many it feeds. A column whose heading is already a <!--/* "Small" doesn't tell anyone how many it feeds. A column whose heading is already a
head count leaves this blank rather than saying it twice. */--> head count leaves this blank rather than saying it twice. */-->
<p th:if="${tier.serves}" class="mt-1 text-sm text-bakery-600" th:text="${tier.serves}">About 68 people</p> <p th:if="${tier.serves}" class="mt-1 text-sm text-bakery-600" th:text="${tier.serves}">About 68 people</p>
@@ -107,14 +106,13 @@
*/--> */-->
<div class="mt-8 max-w-2xl mx-auto text-center"> <div class="mt-8 max-w-2xl mx-auto text-center">
<div th:if="${!#lists.isEmpty(table.notes)}"> <div th:if="${!#lists.isEmpty(table.notes)}">
<h3 class="text-xs uppercase tracking-[0.15em] text-bakery-500 font-medium">Good to know</h3> <h3 class="label text-bakery-500">Good to know</h3>
<ul class="mt-3 space-y-2 text-sm text-bakery-700"> <ul class="mt-3 space-y-2 text-sm text-bakery-700">
<li th:each="note : ${table.notes}" th:text="${note}">Everything is baked in sixes.</li> <li th:each="note : ${table.notes}" th:text="${note}">Everything is baked in sixes.</li>
</ul> </ul>
</div> </div>
<a th:href="|/contact?about=${#uris.escapeQueryParam(table.name)}|" <a th:href="|/contact?about=${#uris.escapeQueryParam(table.name)}|"
class="mt-6 inline-block border border-bakery-300 hover:bg-bakery-100 text-bakery-800 class="pill-outline mt-6"
px-7 py-3 rounded-full font-medium tracking-wide transition-colors"
th:text="|Ask about ${#strings.toLowerCase(table.name)}|">Ask about office boxes</a> th:text="|Ask about ${#strings.toLowerCase(table.name)}|">Ask about office boxes</a>
</div> </div>
</section> </section>
@@ -131,20 +129,20 @@
<!--/* The terms that apply whichever table you were reading, and the one real call to action. On sage, <!--/* The terms that apply whichever table you were reading, and the one real call to action. On sage,
like the homepage's story band, so the page ends rather than trailing off. */--> like the homepage's story band, so the page ends rather than trailing off. */-->
<section class="bg-bakery-800 text-white py-16 md:py-20"> <section class="section bg-bakery-800 text-white">
<div class="container mx-auto px-4"> <div class="container mx-auto px-4">
<div class="max-w-2xl mx-auto text-center"> <div class="max-w-2xl mx-auto text-center">
<h2 class="font-adbhashitha text-2xl md:text-3xl">Before you order</h2> <h2 class="h3">Before you order</h2>
<ul th:if="${!#lists.isEmpty(menu.notes)}" class="mt-6 space-y-3 text-bakery-100 leading-relaxed"> <ul th:if="${!#lists.isEmpty(menu.notes)}" class="mt-6 space-y-3 text-bakery-100 leading-relaxed">
<li th:each="note : ${menu.notes}" th:text="${note}">These are a guide rather than a menu.</li> <li th:each="note : ${menu.notes}" th:text="${note}">These are a guide rather than a menu.</li>
</ul> </ul>
<div class="mt-10 flex flex-col sm:flex-row gap-3 justify-center"> <div class="mt-10 flex flex-col sm:flex-row gap-3 justify-center">
<a href="/contact" <a href="/contact"
class="bg-bakery-50 hover:bg-white text-bakery-900 px-8 py-3.5 rounded-full font-medium tracking-wide inline-block transition shadow-lg"> class="pill-cream">
Tell us what you need Tell us what you need
</a> </a>
<a href="tel:+13097010660" <a href="tel:+13097010660"
class="border border-bakery-200/60 hover:bg-bakery-50/10 text-bakery-50 px-8 py-3.5 rounded-full font-medium tracking-wide inline-block transition"> class="pill-ghost">
(309) 701-0660 (309) 701-0660
</a> </a>
</div> </div>
+9 -9
View File
@@ -4,7 +4,7 @@
<div th:fragment="content" class="min-h-screen bg-bakery-50"> <div th:fragment="content" class="min-h-screen bg-bakery-50">
<header class="container mx-auto px-4 pt-14 pb-10 md:pt-20 md:pb-12 text-center"> <header class="container mx-auto px-4 pt-14 pb-10 md:pt-20 md:pb-12 text-center">
<h1 class="font-adbhashitha text-4xl md:text-5xl text-bakery-900">Contact us</h1> <h1 class="h1 text-bakery-900">Contact us</h1>
<p class="mt-4 text-bakery-800 max-w-xl mx-auto leading-relaxed"> <p class="mt-4 text-bakery-800 max-w-xl mx-auto leading-relaxed">
Ask about a cake, a date, or an order for a crowd. We read everything that comes in. Ask about a cake, a date, or an order for a crowd. We read everything that comes in.
</p> </p>
@@ -17,8 +17,8 @@
*/--> */-->
<div class="container mx-auto px-4 pb-16 md:pb-24"> <div class="container mx-auto px-4 pb-16 md:pb-24">
<div class="grid gap-8 lg:grid-cols-[1.4fr_1fr] max-w-5xl mx-auto items-start"> <div class="grid gap-8 lg:grid-cols-[1.4fr_1fr] max-w-5xl mx-auto items-start">
<div class="bg-white p-8 md:p-10 rounded-3xl shadow-xs"> <div class="panel p-8 md:p-10">
<h2 class="font-adbhashitha text-2xl md:text-3xl text-bakery-900 mb-2">Get in touch</h2> <h2 class="h3 text-bakery-900 mb-2">Get in touch</h2>
<p class="text-bakery-700 mb-8"> <p class="text-bakery-700 mb-8">
Or call us: <a href="tel:+13097010660" class="underline underline-offset-4 hover:text-bakery-600">(309) 701-0660</a> Or call us: <a href="tel:+13097010660" class="underline underline-offset-4 hover:text-bakery-600">(309) 701-0660</a>
</p> </p>
@@ -52,7 +52,7 @@
</div> </div>
<div class="flex flex-col items-center gap-4"> <div class="flex flex-col items-center gap-4">
<button type="submit" <button type="submit"
class="px-8 py-3 bg-bakery-600 text-white rounded-full tracking-wide hover:bg-bakery-700 transition-colors"> class="pill-sage">
Send message Send message
</button> </button>
@@ -71,18 +71,18 @@
</div> </div>
<aside class="space-y-6"> <aside class="space-y-6">
<div class="bg-white p-6 md:p-8 rounded-3xl shadow-xs"> <div class="panel p-6 md:p-8">
<h2 class="font-adbhashitha text-xl text-bakery-900">Our hours</h2> <h2 class="h4 text-bakery-900">Our hours</h2>
<div class="mt-4 text-bakery-800" <div class="mt-4 text-bakery-800"
th:replace="~{fragments/visit :: hours('text-bakery-800')}"></div> th:replace="~{fragments/visit :: hours('text-bakery-800')}"></div>
</div> </div>
<div class="bg-white p-6 md:p-8 rounded-3xl shadow-xs"> <div class="panel p-6 md:p-8">
<h2 class="font-adbhashitha text-xl text-bakery-900">Find us</h2> <h2 class="h4 text-bakery-900">Find us</h2>
<div class="mt-4 text-bakery-800" <div class="mt-4 text-bakery-800"
th:replace="~{fragments/visit :: address('hover:text-bakery-600 underline underline-offset-4')}"></div> th:replace="~{fragments/visit :: address('hover:text-bakery-600 underline underline-offset-4')}"></div>
</div> </div>
<div class="rounded-3xl border border-bakery-200 p-6 md:p-8"> <div class="rounded-3xl border border-bakery-200 p-6 md:p-8">
<h2 class="font-adbhashitha text-xl text-bakery-900">Ordering for a crowd?</h2> <h2 class="h4 text-bakery-900">Ordering for a crowd?</h2>
<p class="mt-2 text-sm text-bakery-700 leading-relaxed"> <p class="mt-2 text-sm text-bakery-700 leading-relaxed">
Goodie boxes, party packages and wedding desserts have their own page, with sizes and prices. Goodie boxes, party packages and wedding desserts have their own page, with sizes and prices.
</p> </p>
+2 -2
View File
@@ -14,13 +14,13 @@
<body> <body>
<div th:fragment="content" class="bg-bakery-50"> <div th:fragment="content" class="bg-bakery-50">
<div class="container mx-auto px-4 py-24 md:py-32 text-center"> <div class="container mx-auto px-4 py-24 md:py-32 text-center">
<h1 class="font-adbhashitha text-4xl md:text-5xl text-bakery-900 mb-6">Something went wrong</h1> <h1 class="h1 text-bakery-900 mb-6">Something went wrong</h1>
<p class="text-bakery-800 mb-10"> <p class="text-bakery-800 mb-10">
That is our fault, not yours. Try again in a moment, or call us on That is our fault, not yours. Try again in a moment, or call us on
<a href="tel:+13097010660" class="underline underline-offset-4">(309) 701-0660</a>. <a href="tel:+13097010660" class="underline underline-offset-4">(309) 701-0660</a>.
</p> </p>
<a href="/" <a href="/"
class="bg-bakery-600 hover:bg-bakery-700 text-white px-8 py-3.5 rounded-full font-medium tracking-wide inline-block transition"> class="pill-sage">
Back home Back home
</a> </a>
</div> </div>
+3 -3
View File
@@ -17,7 +17,7 @@
<body> <body>
<div th:fragment="content" class="bg-bakery-50"> <div th:fragment="content" class="bg-bakery-50">
<div class="container mx-auto px-4 py-24 md:py-32 text-center"> <div class="container mx-auto px-4 py-24 md:py-32 text-center">
<h1 class="font-adbhashitha text-4xl md:text-5xl text-bakery-900 mb-6"> <h1 class="h1 text-bakery-900 mb-6">
We could not find that page We could not find that page
</h1> </h1>
<p class="text-bakery-800 mb-10"> <p class="text-bakery-800 mb-10">
@@ -25,11 +25,11 @@
</p> </p>
<div class="flex flex-col sm:flex-row gap-3 justify-center"> <div class="flex flex-col sm:flex-row gap-3 justify-center">
<a href="/" <a href="/"
class="bg-bakery-600 hover:bg-bakery-700 text-white px-8 py-3.5 rounded-full font-medium tracking-wide inline-block transition"> class="pill-sage">
Back home Back home
</a> </a>
<a href="/products" <a href="/products"
class="border border-bakery-300 hover:bg-bakery-100 text-bakery-700 px-8 py-3.5 rounded-full font-medium tracking-wide inline-block transition"> class="pill-outline">
See the menu See the menu
</a> </a>
</div> </div>
@@ -9,7 +9,7 @@
</div> </div>
<div> <div>
<h3 class="font-adbhashitha text-sm uppercase tracking-[0.18em] text-bakery-300 mb-4">Navigation</h3> <h3 class="label text-bakery-300 mb-4 font-adbhashitha">Navigation</h3>
<ul class="space-y-2"> <ul class="space-y-2">
<li><a href="/products" class="hover:text-white transition">Our Products</a></li> <li><a href="/products" class="hover:text-white transition">Our Products</a></li>
<li><a href="/catering" class="hover:text-white transition">Goodie Boxes &amp; Catering</a></li> <li><a href="/catering" class="hover:text-white transition">Goodie Boxes &amp; Catering</a></li>
@@ -19,7 +19,7 @@
</div> </div>
<div> <div>
<h3 class="font-adbhashitha text-sm uppercase tracking-[0.18em] text-bakery-300 mb-4">Visit</h3> <h3 class="label text-bakery-300 mb-4 font-adbhashitha">Visit</h3>
<div th:replace="~{fragments/visit :: address('hover:text-white transition')}"></div> <div th:replace="~{fragments/visit :: address('hover:text-white transition')}"></div>
<!--/* The hours are the thing people come to a bakery's site for, and the footer is on every <!--/* The hours are the thing people come to a bakery's site for, and the footer is on every
page. */--> page. */-->
@@ -22,7 +22,7 @@
<div th:replace="~{fragments/lockup :: small('text-bakery-700')}"></div> <div th:replace="~{fragments/lockup :: small('text-bakery-700')}"></div>
<nav class="hidden md:flex items-center gap-8"> <nav class="hidden md:flex items-center gap-8">
<th:block th:replace="~{:: links('text-sm uppercase tracking-[0.15em] text-bakery-700 hover:text-bakery-900 transition')}"></th:block> <th:block th:replace="~{:: links('label text-bakery-700 hover:text-bakery-900 transition-colors')}"></th:block>
</nav> </nav>
<details class="group md:hidden shrink-0"> <details class="group md:hidden shrink-0">
+12 -12
View File
@@ -4,16 +4,16 @@
<div th:fragment="content" class="bg-bakery-50"> <div th:fragment="content" class="bg-bakery-50">
<!--/* The shop's own front, which is what the story is about. Same band as the catering page. */--> <!--/* The shop's own front, which is what the story is about. Same band as the catering page. */-->
<section class="relative flex items-center py-14 md:py-20 bg-bakery-900 text-white overflow-hidden"> <section class="band relative flex items-center bg-bakery-900 text-white overflow-hidden">
<img th:src="${photos.of('gallery/Outside.webp')}" alt="" <img th:src="${photos.of('gallery/Outside.webp')}" alt=""
class="absolute inset-0 w-full h-full object-cover object-bottom blur-[3px] scale-110"> class="absolute inset-0 w-full h-full object-cover object-bottom blur-[3px] scale-110">
<div class="absolute inset-0 bg-bakery-900/80"></div> <div class="absolute inset-0 bg-bakery-900/80"></div>
<div class="absolute inset-0 bg-gradient-to-t from-bakery-900 via-bakery-800/60 to-bakery-900/80"></div> <div class="absolute inset-0 bg-gradient-to-t from-bakery-900 via-bakery-800/60 to-bakery-900/80"></div>
<div class="relative container mx-auto px-4 w-full min-w-0"> <div class="relative container mx-auto px-4 w-full min-w-0">
<div class="max-w-3xl mx-auto text-center"> <div class="max-w-3xl mx-auto text-center">
<p class="text-xs sm:text-sm uppercase tracking-[0.2em] text-bakery-200">Since 2024</p> <p class="eyebrow text-bakery-200">Since 2024</p>
<h1 class="mt-3 font-adbhashitha text-4xl md:text-5xl text-bakery-50">Our story</h1> <h1 class="h1 mt-3 text-bakery-50">Our story</h1>
<p class="mt-5 text-base sm:text-lg text-bakery-100 text-balance leading-relaxed"> <p class="lede mt-5 text-bakery-100">
215 E Main Street, in the middle of downtown Princeville. 215 E Main Street, in the middle of downtown Princeville.
</p> </p>
</div> </div>
@@ -25,7 +25,7 @@
<div class="container mx-auto px-4 pb-16 md:pb-24"> <div class="container mx-auto px-4 pb-16 md:pb-24">
<div class="max-w-2xl mx-auto space-y-12"> <div class="max-w-2xl mx-auto space-y-12">
<section> <section>
<h2 class="font-adbhashitha text-2xl md:text-3xl text-bakery-900 mb-4">How it started</h2> <h2 class="h3 text-bakery-900 mb-4">How it started</h2>
<p class="text-bakery-800 leading-relaxed"> <p class="text-bakery-800 leading-relaxed">
Morissa Bennett opened The Vine in 2024, at 215 E Main Street, in the middle of downtown Morissa Bennett opened The Vine in 2024, at 215 E Main Street, in the middle of downtown
Princeville. The plan was not complicated. Bake it ourselves, sell it ourselves, and keep Princeville. The plan was not complicated. Bake it ourselves, sell it ourselves, and keep
@@ -34,7 +34,7 @@
</section> </section>
<section> <section>
<h2 class="font-adbhashitha text-2xl md:text-3xl text-bakery-900 mb-4">What we make</h2> <h2 class="h3 text-bakery-900 mb-4">What we make</h2>
<p class="text-bakery-800 leading-relaxed"> <p class="text-bakery-800 leading-relaxed">
We opened with coffee and pastries. The menu kept growing. Now there are cinnamon rolls We opened with coffee and pastries. The menu kept growing. Now there are cinnamon rolls
and caramel rolls, scones, cookie bars, macarons, brownies, and pies, plus sandwiches and and caramel rolls, scones, cookie bars, macarons, brownies, and pies, plus sandwiches and
@@ -43,7 +43,7 @@
</section> </section>
<section> <section>
<h2 class="font-adbhashitha text-2xl md:text-3xl text-bakery-900 mb-4">The cakes are the fun part</h2> <h2 class="h3 text-bakery-900 mb-4">The cakes are the fun part</h2>
<p class="text-bakery-800 leading-relaxed"> <p class="text-bakery-800 leading-relaxed">
Cakes and decorated cookies are made to order, which means we mostly bake whatever Cakes and decorated cookies are made to order, which means we mostly bake whatever
Princeville is celebrating that week. We have done a tractor, a cow, a 76th birthday, a Princeville is celebrating that week. We have done a tractor, a cow, a 76th birthday, a
@@ -54,7 +54,7 @@
</section> </section>
<section> <section>
<h2 class="font-adbhashitha text-2xl md:text-3xl text-bakery-900 mb-4">Around town</h2> <h2 class="h3 text-bakery-900 mb-4">Around town</h2>
<p class="text-bakery-800 leading-relaxed"> <p class="text-bakery-800 leading-relaxed">
We turn out for Christmas in the Village every year and for other civic events, and the We turn out for Christmas in the Village every year and for other civic events, and the
Princeville Civic Association counts us among the town&apos;s small businesses. Enjoy Princeville Civic Association counts us among the town&apos;s small businesses. Enjoy
@@ -66,21 +66,21 @@
</div> </div>
<!--/* Somewhere to go after reading: the story is a preamble to an order. */--> <!--/* Somewhere to go after reading: the story is a preamble to an order. */-->
<section class="bg-bakery-800 text-white py-16 md:py-20"> <section class="section bg-bakery-800 text-white">
<div class="container mx-auto px-4"> <div class="container mx-auto px-4">
<div class="max-w-2xl mx-auto text-center"> <div class="max-w-2xl mx-auto text-center">
<h2 class="font-adbhashitha text-2xl md:text-3xl">Come and see us</h2> <h2 class="h3">Come and see us</h2>
<p class="mt-4 text-bakery-100 leading-relaxed"> <p class="mt-4 text-bakery-100 leading-relaxed">
We open at 7:00am, Tuesday through Saturday. If you are after something for a date in the We open at 7:00am, Tuesday through Saturday. If you are after something for a date in the
diary, it is worth asking early. diary, it is worth asking early.
</p> </p>
<div class="mt-8 flex flex-col sm:flex-row gap-3 justify-center"> <div class="mt-8 flex flex-col sm:flex-row gap-3 justify-center">
<a href="/products" <a href="/products"
class="bg-bakery-50 hover:bg-white text-bakery-900 px-8 py-3.5 rounded-full font-medium tracking-wide inline-block transition shadow-lg"> class="pill-cream">
See what we make See what we make
</a> </a>
<a href="/contact" <a href="/contact"
class="border border-bakery-200/60 hover:bg-bakery-50/10 text-bakery-50 px-8 py-3.5 rounded-full font-medium tracking-wide inline-block transition"> class="pill-ghost">
Get in touch Get in touch
</a> </a>
</div> </div>
+18 -18
View File
@@ -18,16 +18,16 @@
<div class="relative container mx-auto px-4 w-full min-w-0"> <div class="relative container mx-auto px-4 w-full min-w-0">
<div class="max-w-3xl mx-auto text-center"> <div class="max-w-3xl mx-auto text-center">
<div th:replace="~{fragments/lockup :: large('text-bakery-50 mb-10')}"></div> <div th:replace="~{fragments/lockup :: large('text-bakery-50 mb-10')}"></div>
<p class="text-lg sm:text-xl text-bakery-100 mb-10 text-balance leading-relaxed"> <p class="lede sm:text-xl text-bakery-100 mb-10">
A coffeehouse and bakery in downtown Princeville, Illinois. A coffeehouse and bakery in downtown Princeville, Illinois.
</p> </p>
<div class="flex flex-col sm:flex-row gap-3 justify-center"> <div class="flex flex-col sm:flex-row gap-3 justify-center">
<a href="/products" <a href="/products"
class="bg-bakery-50 hover:bg-white text-bakery-900 px-8 py-3.5 rounded-full font-medium tracking-wide inline-block transition shadow-lg"> class="pill-cream">
See the menu See the menu
</a> </a>
<a href="#visit" <a href="#visit"
class="border border-bakery-200/60 hover:bg-bakery-50/10 text-bakery-50 px-8 py-3.5 rounded-full font-medium tracking-wide inline-block transition"> class="pill-ghost">
Visit us Visit us
</a> </a>
</div> </div>
@@ -36,28 +36,28 @@
</section> </section>
<!--/* What people come in for */--> <!--/* What people come in for */-->
<section class="py-16 md:py-24 bg-bakery-50"> <section class="section bg-bakery-50">
<div class="container mx-auto px-4"> <div class="container mx-auto px-4">
<h2 class="font-adbhashitha text-3xl md:text-4xl text-center text-bakery-900 mb-12">What people come in for</h2> <h2 class="h2 text-center text-bakery-900 mb-12">What people come in for</h2>
<div class="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-6 md:gap-8"> <div class="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-6 md:gap-8">
<div th:each="item : ${ {'Cinnamon Rolls', 'Sugar Cookies', 'Cakes'} }" <div th:each="item : ${ {'Cinnamon Rolls', 'Sugar Cookies', 'Cakes'} }"
class="group text-center bg-white rounded-3xl overflow-hidden shadow-xs hover:shadow-lg hover:-translate-y-1 transition duration-300"> class="group panel-lift overflow-hidden text-center hover:-translate-y-1">
<div class="overflow-hidden"> <div class="overflow-hidden">
<!--/* The filenames carry the spaces in these names; SitePhotos encodes them. */--> <!--/* The filenames carry the spaces in these names; SitePhotos encodes them. */-->
<img th:src="${photos.of('gallery/' + item + '.webp')}" th:alt="${item}" width="600" height="400" loading="lazy" <img th:src="${photos.of('gallery/' + item + '.webp')}" th:alt="${item}" width="600" height="400" loading="lazy"
class="w-full h-56 object-cover transition-transform duration-500 group-hover:scale-105"> class="w-full h-56 object-cover transition-transform duration-500 group-hover:scale-105">
</div> </div>
<h3 class="font-adbhashitha text-xl md:text-2xl text-bakery-800 py-6 tracking-wide" th:text="${item}">Cinnamon Rolls</h3> <h3 class="h4 md:text-2xl text-bakery-800 py-6" th:text="${item}">Cinnamon Rolls</h3>
</div> </div>
</div> </div>
</div> </div>
</section> </section>
<!--/* Our story */--> <!--/* Our story */-->
<section class="py-16 md:py-24 bg-bakery-800 text-white"> <section class="section bg-bakery-800 text-white">
<div class="container mx-auto px-4"> <div class="container mx-auto px-4">
<div class="max-w-3xl mx-auto text-center"> <div class="max-w-3xl mx-auto text-center">
<h2 class="font-adbhashitha text-3xl md:text-4xl mb-8" style="letter-spacing: 0.01em">Our story</h2> <h2 class="h2 mb-8" style="letter-spacing: 0.01em">Our story</h2>
<p class="text-base md:text-lg text-bakery-100 mb-8 leading-relaxed"> <p class="text-base md:text-lg text-bakery-100 mb-8 leading-relaxed">
Morissa Bennett opened The Vine in 2024. We bake in our own kitchen on Main Street: Morissa Bennett opened The Vine in 2024. We bake in our own kitchen on Main Street:
cinnamon rolls, cookies, custom cakes, sandwiches, paninis, and coffee. cinnamon rolls, cookies, custom cakes, sandwiches, paninis, and coffee.
@@ -74,20 +74,20 @@
found it by reading the nav. Photo on one side, the offer on the other, and the three tables named so found it by reading the nav. Photo on one side, the offer on the other, and the three tables named so
the link is worth following. the link is worth following.
*/--> */-->
<section class="py-16 md:py-24 bg-bakery-50"> <section class="section bg-bakery-50">
<div class="container mx-auto px-4"> <div class="container mx-auto px-4">
<div class="grid md:grid-cols-2 gap-8 md:gap-12 items-center max-w-5xl mx-auto"> <div class="grid md:grid-cols-2 gap-8 md:gap-12 items-center max-w-5xl mx-auto">
<img th:src="${photos.of('gallery/Sugar Cookies.webp')}" alt="Iced sugar cookies from The Vine" <img th:src="${photos.of('gallery/Sugar Cookies.webp')}" alt="Iced sugar cookies from The Vine"
width="800" height="600" loading="lazy" width="800" height="600" loading="lazy"
class="w-full h-64 md:h-80 object-cover rounded-3xl shadow-xs"> class="w-full h-64 md:h-80 object-cover rounded-3xl shadow-xs">
<div> <div>
<h2 class="font-adbhashitha text-3xl md:text-4xl text-bakery-900">Ordering for a crowd?</h2> <h2 class="h2 text-bakery-900">Ordering for a crowd?</h2>
<p class="mt-4 text-bakery-800 leading-relaxed"> <p class="mt-4 text-bakery-800 leading-relaxed">
Goodie boxes for the office, packages for a party, and cakes and desserts for a wedding — Goodie boxes for the office, packages for a party, and cakes and desserts for a wedding —
with prices, sizes and what each one includes, so you can see where to start. with prices, sizes and what each one includes, so you can see where to start.
</p> </p>
<a href="/catering" <a href="/catering"
class="mt-6 inline-block bg-bakery-600 hover:bg-bakery-700 text-white px-8 py-3.5 rounded-full font-medium tracking-wide transition"> class="pill-sage mt-6">
Goodie boxes &amp; catering Goodie boxes &amp; catering
</a> </a>
</div> </div>
@@ -96,16 +96,16 @@
</section> </section>
<!--/* Visit us */--> <!--/* Visit us */-->
<section id="visit" class="py-16 md:py-24 bg-bakery-50 scroll-mt-24"> <section id="visit" class="section bg-bakery-50 scroll-mt-24">
<div class="container mx-auto px-4"> <div class="container mx-auto px-4">
<h2 class="font-adbhashitha text-3xl md:text-4xl text-center text-bakery-900 mb-12">Visit us</h2> <h2 class="h2 text-center text-bakery-900 mb-12">Visit us</h2>
<div class="grid grid-cols-1 md:grid-cols-2 gap-8 md:gap-12 max-w-4xl mx-auto"> <div class="grid grid-cols-1 md:grid-cols-2 gap-8 md:gap-12 max-w-4xl mx-auto">
<div class="bg-white rounded-3xl p-6 md:p-8"> <div class="panel p-6 md:p-8">
<h2 class="font-adbhashitha text-2xl md:text-3xl text-bakery-900 mb-6">Our hours</h2> <h2 class="h3 text-bakery-900 mb-6">Our hours</h2>
<div th:replace="~{fragments/visit :: hours('text-bakery-800')}"></div> <div th:replace="~{fragments/visit :: hours('text-bakery-800')}"></div>
</div> </div>
<div class="bg-white rounded-3xl p-6 md:p-8"> <div class="panel p-6 md:p-8">
<h2 class="font-adbhashitha text-2xl md:text-3xl text-bakery-900 mb-6">Find us</h2> <h2 class="h3 text-bakery-900 mb-6">Find us</h2>
<div class="text-bakery-800" <div class="text-bakery-800"
th:replace="~{fragments/visit :: address('hover:text-bakery-600 underline underline-offset-4')}"></div> th:replace="~{fragments/visit :: address('hover:text-bakery-600 underline underline-offset-4')}"></div>
</div> </div>
+9 -12
View File
@@ -4,7 +4,7 @@
<div th:fragment="content" class="min-h-screen bg-bakery-50"> <div th:fragment="content" class="min-h-screen bg-bakery-50">
<header class="container mx-auto px-4 pt-14 pb-10 md:pt-20 md:pb-12 text-center"> <header class="container mx-auto px-4 pt-14 pb-10 md:pt-20 md:pb-12 text-center">
<h1 class="font-adbhashitha text-4xl md:text-5xl text-bakery-900">Our products</h1> <h1 class="h1 text-bakery-900">Our products</h1>
<p class="mt-4 text-bakery-800 max-w-2xl mx-auto leading-relaxed"> <p class="mt-4 text-bakery-800 max-w-2xl mx-auto leading-relaxed">
What comes out of the kitchen on Main Street. Cakes and decorated cookies are made to order, so What comes out of the kitchen on Main Street. Cakes and decorated cookies are made to order, so
most of what follows started as somebody describing what they wanted. most of what follows started as somebody describing what they wanted.
@@ -23,10 +23,7 @@
th:href="${category == 'All' ? '/products' : '/products?category=' + #uris.escapeQueryParam(category)}" th:href="${category == 'All' ? '/products' : '/products?category=' + #uris.escapeQueryParam(category)}"
th:text="${category}" th:text="${category}"
th:aria-current="${category == selected ? 'page' : null}" th:aria-current="${category == selected ? 'page' : null}"
th:class="'px-6 py-2 rounded-full border text-sm uppercase tracking-[0.12em] transition-colors ' th:class="${category == selected} ? 'chip-on' : 'chip'">All</a>
+ (${category == selected}
? 'bg-bakery-600 text-white border-bakery-600'
: 'bg-white border-bakery-300 text-bakery-700 hover:bg-bakery-100')">All</a>
</div> </div>
<!--/* Deliberately unanimated, as before: filtering used to run a layout reflow plus an enter/exit <!--/* Deliberately unanimated, as before: filtering used to run a layout reflow plus an enter/exit
@@ -34,13 +31,13 @@
responding. The only motion left is the shadow on hover. */--> responding. The only motion left is the shadow on hover. */-->
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8"> <div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
<div th:each="product : ${products}" <div th:each="product : ${products}"
class="group bg-white rounded-3xl overflow-hidden shadow-xs transition-shadow duration-300 hover:shadow-lg"> class="group panel-lift overflow-hidden">
<div class="relative w-full overflow-hidden"> <div class="relative w-full overflow-hidden">
<div th:replace="~{:: gallery(${product})}"></div> <div th:replace="~{:: gallery(${product})}"></div>
</div> </div>
<div class="p-6 text-center"> <div class="p-6 text-center">
<h3 class="font-adbhashitha text-xl text-bakery-900 mb-2 tracking-wide" th:text="${product.name}">Name</h3> <h3 class="h4 text-bakery-900 mb-2" th:text="${product.name}">Name</h3>
<span class="text-xs uppercase tracking-[0.15em] text-bakery-600" th:text="${product.category}">Category</span> <span class="label text-bakery-600" th:text="${product.category}">Category</span>
</div> </div>
</div> </div>
</div> </div>
@@ -52,21 +49,21 @@
</div> </div>
<!--/* A catalogue with no way to order from it is a gallery. */--> <!--/* A catalogue with no way to order from it is a gallery. */-->
<section class="bg-bakery-800 text-white py-16 md:py-20"> <section class="section bg-bakery-800 text-white">
<div class="container mx-auto px-4"> <div class="container mx-auto px-4">
<div class="max-w-2xl mx-auto text-center"> <div class="max-w-2xl mx-auto text-center">
<h2 class="font-adbhashitha text-2xl md:text-3xl">Want one of these, or something else?</h2> <h2 class="h3">Want one of these, or something else?</h2>
<p class="mt-4 text-bakery-100 leading-relaxed"> <p class="mt-4 text-bakery-100 leading-relaxed">
Cakes and decorated cookies are made to order. Tell us the date and roughly what you have in Cakes and decorated cookies are made to order. Tell us the date and roughly what you have in
mind — a tractor, a retirement, the class of 1964 — and we will work from there. mind — a tractor, a retirement, the class of 1964 — and we will work from there.
</p> </p>
<div class="mt-8 flex flex-col sm:flex-row gap-3 justify-center"> <div class="mt-8 flex flex-col sm:flex-row gap-3 justify-center">
<a href="/contact" <a href="/contact"
class="bg-bakery-50 hover:bg-white text-bakery-900 px-8 py-3.5 rounded-full font-medium tracking-wide inline-block transition shadow-lg"> class="pill-cream">
Ask about an order Ask about an order
</a> </a>
<a href="/catering" <a href="/catering"
class="border border-bakery-200/60 hover:bg-bakery-50/10 text-bakery-50 px-8 py-3.5 rounded-full font-medium tracking-wide inline-block transition"> class="pill-ghost">
Ordering for a crowd Ordering for a crowd
</a> </a>
</div> </div>
+36
View File
@@ -38,3 +38,39 @@ body {
background-color: var(--color-bakery-300); background-color: var(--color-bakery-300);
color: var(--color-bakery-900); color: var(--color-bakery-900);
} }
/*
* FOCUS. One visible ring for everything you can reach with a keyboard, as an outline rather than a
* shadow so it shows up on cream, on white and on the sage bands without needing three variants.
* :where() keeps its specificity at zero, so a utility can still override it.
*/
:where(a, button, summary, input, select, textarea, [tabindex]):focus-visible {
outline: 2px solid var(--color-bakery-500);
outline-offset: 3px;
border-radius: 3px;
}
/*
* THE Z-INDEX SCALE, in full. Two layers is all this site has, and writing them down is what stops a
* third being invented at 9999:
*
* 40 the sticky header
* 30 the mobile menu panel, which is a child of the header and therefore already inside its stacking
* context — it can only ever sit under the bar, which is exactly where it belongs
*
* Everything else stacks in document order: the hero's tint sits over its photo because it comes after
* it, and the product-card arrows sit over the photos for the same reason.
*/
/*
* A native select still gets its own arrow from the platform, in the platform's colour. This is the one
* control the admin uses that we don't draw ourselves, so it gets the site's chevron instead.
*/
select.field {
appearance: none;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%234a5740' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M6 9l6 6 6-6'/%3E%3C/svg%3E");
background-repeat: no-repeat;
background-position: right 0.6rem center;
background-size: 1.1rem;
padding-right: 2.4rem;
}
+64
View File
@@ -58,3 +58,67 @@ summary {
summary::-webkit-details-marker { summary::-webkit-details-marker {
display: none; display: none;
} }
/*
* SURFACES. Every white box on the site was "bg-white rounded-3xl shadow-xs" written out again, which is
* how two of them end up with different corners. `panel` is that box; `panel-lift` is one you can click
* or hover, and it lifts.
*/
@utility panel {
@apply bg-white rounded-3xl shadow-xs;
}
@utility panel-lift {
@apply panel transition duration-300 hover:shadow-lg;
}
/*
* BUTTONS. One shape — a full pill, generous, letterspaced — in four colourways, because the site puts
* buttons on cream and on sage and they cannot be the same colour on both.
*
* pill-sage filled, on a light background pill-outline quiet, on a light background
* pill-cream filled, on a sage background pill-ghost quiet, on a sage background
*/
@utility pill {
@apply inline-flex items-center justify-center gap-2 rounded-full
px-8 py-3.5 font-medium tracking-wide transition-colors;
}
@utility pill-sage {
@apply pill bg-bakery-600 text-white hover:bg-bakery-700;
}
@utility pill-cream {
@apply pill bg-bakery-50 text-bakery-900 hover:bg-white shadow-lg;
}
@utility pill-outline {
@apply pill border border-bakery-300 text-bakery-800 hover:bg-bakery-100;
}
@utility pill-ghost {
@apply pill border border-bakery-200/60 text-bakery-50 hover:bg-bakery-50/10;
}
/* Smaller, for a filter row rather than a call to action. */
@utility chip {
@apply inline-flex items-center rounded-full border px-6 py-2 label
bg-white border-bakery-300 text-bakery-700 transition-colors hover:bg-bakery-100;
}
@utility chip-on {
@apply chip bg-bakery-600 border-bakery-600 text-white hover:bg-bakery-600;
}
/*
* RHYTHM. Two vertical sizes, so the page has a beat instead of nine hand-picked paddings: `section` for
* a block of content, `band` for a photographic or coloured strip, which sits tighter because its
* background is already doing the separating.
*/
@utility section {
@apply py-16 md:py-24;
}
@utility band {
@apply py-14 md:py-20;
}
+1
View File
@@ -12,6 +12,7 @@
@import "./theme.css"; @import "./theme.css";
@import "./base.css"; @import "./base.css";
@import "./type.css";
@import "./components.css"; @import "./components.css";
@import "./admin.css"; @import "./admin.css";
+71
View File
@@ -0,0 +1,71 @@
/*
* The typographic ladder, taken from the logo and walked down to something you can read a paragraph in.
*
* The lockup is the brand's whole voice: "The Vine" in LeJour Script over COFFEEHOUSE + BAKERY in
* AdBhashitha, letterspaced. Everything below is that idea, made progressively more readable:
*
* 1. .wordmark LeJour Script the name, and nothing else. A script face is unreadable at length,
* so it never leaves the lockup.
* 2. .h1 .h2 .h3 AdBhashitha the lockup's second font, carrying every heading. It keeps the
* wordmark's 0.01em letter-spacing so a heading sits on the same
* rhythm as the logo above it.
* 3. .eyebrow Raleway, caps the hinge: sans, but letterspaced like the lockup's tagline. Used
* .label above a heading and on small labels, it carries the brand into
* places a display face would be shouting.
* 4. .lede, body Raleway plain, and left alone.
*
* Sizes are declared here rather than in the templates so a heading is a decision made once. None of
* these set a colour: the same heading appears in sage on cream and in cream on sage, and a class that
* decided that would be fought with a utility every time.
*/
@utility wordmark {
font-family: var(--font-lejour);
letter-spacing: 0.01em;
}
@utility h1 {
@apply font-adbhashitha text-4xl md:text-5xl leading-tight text-balance;
letter-spacing: 0.01em;
}
@utility h2 {
@apply font-adbhashitha text-3xl md:text-4xl leading-tight text-balance;
letter-spacing: 0.01em;
}
@utility h3 {
@apply font-adbhashitha text-2xl md:text-3xl leading-snug;
letter-spacing: 0.01em;
}
@utility h4 {
@apply font-adbhashitha text-xl leading-snug;
letter-spacing: 0.01em;
}
/* Above a heading, or as a section's own small heading. */
@utility eyebrow {
@apply text-xs sm:text-sm uppercase font-medium;
letter-spacing: 0.2em;
}
/* Smaller, tighter: a column heading, a "good to know", a category chip. */
@utility label {
@apply text-xs uppercase font-medium;
letter-spacing: 0.15em;
}
/* The sentence under a page title. One size up from body, and balanced so it doesn't leave an orphan. */
@utility lede {
@apply text-base sm:text-lg leading-relaxed text-balance;
}
/*
* A price. The display face at a size nothing else uses, because on a catering card the number is the
* thing the eye should land on after the size.
*/
@utility price {
@apply font-adbhashitha text-4xl leading-none;
letter-spacing: 0.01em;
}