Everything on the shared names — the copy-paste is gone

The ported markup was written a page at a time, so the same thing was said several ways. Counted before
touching anything: nine variants of the page gutter, seven max-widths, six link styles, two definitions of
an input, and one stray shadow.

Now: `container` (which already centres and pads), `measure`/`measure-wide`, `link`/`link-plain`/
`link-on-dark`, one `field`, `photo`, `page-head`. Nothing in a template writes `bg-white rounded-…
shadow-…` or `underline underline-offset-4` any more — 119 uses of `panel`, 82 of `label`, 52 of
`panel-lift`, 17 of `link`.

TWO REAL FIXES FELL OUT OF IT, not just tidying:

- Every section said `container mx-auto px-4`: `mx-auto` twice, and a px-4 that beat the responsive
  padding inside `container`. The gutter was 16px at every width, including on a 27-inch screen. Removing
  the copy-paste restores sm:px-6 lg:px-8, which is most of why the pages now breathe.
- The public form and the admin had grown two different inputs — cream vs white, ring-2 vs ring-1, py-2.5
  vs py-2, and different text sizes. There is one `field` now, and the same focus ring as everything else.

The admin uses the same names where they are the same thing (`link`, `field`, `h4`, `--radius-*`) and
keeps its own denser buttons, because it is a tool and not a shop front.

53 tests green. Every class used across all nine pages resolves in the compiled stylesheet — that check is
what makes a sweep this size safe to do with a script.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
This commit is contained in:
2026-07-26 18:43:40 -05:00
co-authored by Claude Opus 5
parent c4cd19e175
commit 9e80590687
15 changed files with 129 additions and 64 deletions
@@ -28,7 +28,7 @@
<div class="flex-1 min-w-60">
<a th:href="@{/admin/catering/tables/{id}(id=${table.id})}"
class="font-adbhashitha text-lg text-bakery-900 underline underline-offset-4"
class="link h4 text-bakery-900"
th:text="${table.name}">Office</a>
<p class="text-sm text-bakery-600">
<span th:text="|${#lists.size(table.tiers)} column${#lists.size(table.tiers) == 1 ? '' : 's'}|">3 columns</span>,
@@ -26,8 +26,8 @@
<div>
<h1 class="font-lejour text-4xl text-bakery-700">The Vine</h1>
<nav class="mt-1 flex flex-wrap gap-4 text-sm">
<a href="/admin" class="text-bakery-700 underline underline-offset-4 hover:text-bakery-900">The catalogue</a>
<a href="/admin/catering" class="text-bakery-700 underline underline-offset-4 hover:text-bakery-900">Goodie boxes &amp; catering</a>
<a href="/admin" class="link">The catalogue</a>
<a href="/admin/catering" class="link">Goodie boxes &amp; catering</a>
</nav>
</div>
<div class="flex items-center gap-2">
@@ -21,7 +21,7 @@
<div class="card">
<div class="flex flex-wrap items-baseline justify-between gap-2">
<h2 class="card-heading">This table</h2>
<a href="/admin/catering" class="text-sm text-bakery-700 underline underline-offset-4">All tables</a>
<a href="/admin/catering" class="link text-sm">All tables</a>
</div>
<div class="mt-3 grid gap-2 sm:grid-cols-[14rem_1fr]">
+8 -8
View File
@@ -14,7 +14,7 @@
<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="relative container mx-auto px-4 w-full min-w-0">
<div class="relative container w-full min-w-0">
<div class="max-w-3xl mx-auto text-center">
<p class="eyebrow text-bakery-200">Order ahead</p>
<h1 class="h1 mt-3 text-bakery-50">Goodie boxes &amp; catering</h1>
@@ -26,9 +26,9 @@
</section>
<div class="bg-bakery-50">
<div class="container mx-auto px-4 pt-14 pb-16 md:pt-16 md:pb-24">
<div class="container pt-14 pb-16 md:pt-16 md:pb-24">
<p class="max-w-2xl mx-auto text-center text-bakery-800 leading-relaxed">
<p class="measure text-center text-bakery-800 leading-relaxed">
Every one of these is a starting point. Tell us the date, the number of people and what you had in
mind, and we will bake to it.
</p>
@@ -54,7 +54,7 @@
alignment the aggregate guarantees.
*/-->
<section th:each="table : ${menu.packages}">
<header class="max-w-2xl mx-auto text-center">
<header class="measure text-center">
<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}">
Mini pastries for a morning meeting.
@@ -109,7 +109,7 @@
three cards led to the same place, since an enquiry is about the table rather than the size,
and the label read "Ask about the 1520 people" on the tables whose sizes are headcounts.
*/-->
<div class="mt-8 max-w-2xl mx-auto text-center">
<div class="mt-8 measure text-center">
<div th:if="${!#lists.isEmpty(table.notes)}">
<h3 class="label text-bakery-500">Good to know</h3>
<ul class="mt-3 space-y-2 text-sm text-bakery-700">
@@ -125,7 +125,7 @@
<!--/* Only shows if every table has been emptied out in the admin. Better than a bare heading. */-->
<p th:if="${#lists.isEmpty(menu.packages)}" class="text-center text-bakery-800">
Our catering list is being updated. Call us on
<a href="tel:+13097010660" class="underline underline-offset-4">(309) 701-0660</a> and we will
<a href="tel:+13097010660" class="link-plain">(309) 701-0660</a> and we will
talk it through.
</p>
</div>
@@ -135,8 +135,8 @@
<!--/* 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. */-->
<section class="section bg-bakery-800 text-white">
<div class="container mx-auto px-4">
<div class="max-w-2xl mx-auto text-center">
<div class="container">
<div class="measure text-center">
<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">
<li th:each="note : ${menu.notes}" th:text="${note}">These are a guide rather than a menu.</li>
+10 -11
View File
@@ -3,7 +3,7 @@
<body>
<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="page-head">
<h1 class="h1 text-bakery-900">Contact us</h1>
<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.
@@ -15,12 +15,12 @@
where we are, and the phone number. They were on the homepage and in the footer but not here, which
made this page a form and nothing else.
*/-->
<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="container pb-16 md:pb-24">
<div class="grid gap-8 lg:grid-cols-[1.4fr_1fr] measure-wide items-start">
<div class="panel p-8 md:p-10">
<h2 class="h3 text-bakery-900 mb-2">Get in touch</h2>
<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="link-plain">(309) 701-0660</a>
</p>
<!--/*
@@ -38,17 +38,17 @@
<div class="mb-4">
<label class="block text-sm font-medium text-bakery-800 mb-2" for="name">Name</label>
<input type="text" id="name" name="name" th:value="${name}" required
class="w-full px-4 py-2.5 bg-bakery-50 border border-bakery-200 rounded-field focus:outline-none focus:ring-2 focus:ring-bakery-500 focus:border-bakery-500">
class="field">
</div>
<div class="mb-4">
<label class="block text-sm font-medium text-bakery-800 mb-2" for="email">Email</label>
<input type="email" id="email" name="email" th:value="${email}" required
class="w-full px-4 py-2.5 bg-bakery-50 border border-bakery-200 rounded-field focus:outline-none focus:ring-2 focus:ring-bakery-500 focus:border-bakery-500">
class="field">
</div>
<div class="mb-4">
<label class="block text-sm font-medium text-bakery-800 mb-2" for="message">Message</label>
<textarea id="message" name="message" rows="5" required th:text="${message}"
class="w-full px-4 py-2.5 bg-bakery-50 border border-bakery-200 rounded-field focus:outline-none focus:ring-2 focus:ring-bakery-500 focus:border-bakery-500"></textarea>
class="field"></textarea>
</div>
<div class="flex flex-col items-center gap-4">
<button type="submit"
@@ -60,7 +60,7 @@
<p th:if="${error}" role="alert" class="text-center text-red-700">
<span th:text="${error}">Could not send the message.</span>
Please call us on
<a href="tel:+13097010660" class="underline underline-offset-4">(309) 701-0660</a>.
<a href="tel:+13097010660" class="link-plain">(309) 701-0660</a>.
</p>
</div>
</form>
@@ -79,15 +79,14 @@
<div class="panel p-6 md:p-8">
<h2 class="h4 text-bakery-900">Find us</h2>
<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('link-plain')}"></div>
</div>
<div class="rounded-panel border border-bakery-200 p-6 md:p-8">
<h2 class="h4 text-bakery-900">Ordering for a crowd?</h2>
<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.
</p>
<a href="/catering" class="mt-4 inline-block text-bakery-700 hover:text-bakery-900 font-medium
underline underline-offset-4 transition-colors">
<a href="/catering" class="link mt-4 inline-block font-medium">
Goodie boxes &amp; catering
</a>
</div>
+2 -2
View File
@@ -13,11 +13,11 @@
'/', ~{::content})}">
<body>
<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 py-24 md:py-32 text-center">
<h1 class="h1 text-bakery-900 mb-6">Something went wrong</h1>
<p class="text-bakery-800 mb-10">
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="link-plain">(309) 701-0660</a>.
</p>
<a href="/"
class="pill-sage">
+1 -1
View File
@@ -16,7 +16,7 @@
'/', ~{::content})}">
<body>
<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 py-24 md:py-32 text-center">
<h1 class="h1 text-bakery-900 mb-6">
We could not find that page
</h1>
@@ -2,7 +2,7 @@
<html xmlns:th="http://www.thymeleaf.org">
<body>
<footer th:fragment="footer" class="bg-bakery-900 text-bakery-100">
<div class="container mx-auto px-4 py-14">
<div class="container py-14">
<div class="grid grid-cols-1 md:grid-cols-4 gap-10">
<div class="col-span-1 md:col-span-2 flex items-start">
<div th:replace="~{fragments/lockup :: small('text-bakery-50')}"></div>
@@ -11,16 +11,16 @@
<div>
<h3 class="label text-bakery-300 mb-4 font-adbhashitha">Navigation</h3>
<ul class="space-y-2">
<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="/history" class="hover:text-white transition">Our Story</a></li>
<li><a href="/contact" class="hover:text-white transition">Contact</a></li>
<li><a href="/products" class="hover:text-white transition-colors">Our Products</a></li>
<li><a href="/catering" class="hover:text-white transition-colors">Goodie Boxes &amp; Catering</a></li>
<li><a href="/history" class="hover:text-white transition-colors">Our Story</a></li>
<li><a href="/contact" class="hover:text-white transition-colors">Contact</a></li>
</ul>
</div>
<div>
<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('link-on-dark')}"></div>
<!--/* The hours are the thing people come to a bakery's site for, and the footer is on every
page. */-->
<div class="mt-4 text-sm text-bakery-200"
@@ -17,7 +17,7 @@
here would resolve against the 80px header box and get clipped to a sliver.
*/-->
<header th:fragment="header" class="sticky top-0 z-40 bg-bakery-50/90 backdrop-blur border-b border-bakery-200">
<div class="container mx-auto px-4">
<div class="container">
<div class="flex items-center justify-between gap-2 h-20 md:h-24">
<div th:replace="~{fragments/lockup :: small('text-bakery-700')}"></div>
@@ -37,7 +37,7 @@
</svg>
</summary>
<div class="absolute inset-x-0 top-full z-30 h-[calc(100dvh-5rem)] bg-bakery-50">
<nav class="container mx-auto px-4 flex flex-col">
<nav class="container flex flex-col">
<th:block th:replace="~{:: links('text-bakery-800 hover:text-bakery-600 transition py-4 text-lg border-b border-bakery-100')}"></th:block>
</nav>
</div>
+6 -6
View File
@@ -9,7 +9,7 @@
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-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 w-full min-w-0">
<div class="max-w-3xl mx-auto text-center">
<p class="eyebrow text-bakery-200">Since 2024</p>
<h1 class="h1 mt-3 text-bakery-50">Our story</h1>
@@ -20,10 +20,10 @@
</div>
</section>
<header class="container mx-auto px-4 pt-14 pb-2 md:pt-16 text-center"></header>
<header class="container pt-14 pb-2 md:pt-16 text-center"></header>
<div class="container mx-auto px-4 pb-16 md:pb-24">
<div class="max-w-2xl mx-auto space-y-12">
<div class="container pb-16 md:pb-24">
<div class="measure space-y-12">
<section>
<h2 class="h3 text-bakery-900 mb-4">How it started</h2>
<p class="text-bakery-800 leading-relaxed">
@@ -67,8 +67,8 @@
<!--/* Somewhere to go after reading: the story is a preamble to an order. */-->
<section class="section bg-bakery-800 text-white">
<div class="container mx-auto px-4">
<div class="max-w-2xl mx-auto text-center">
<div class="container">
<div class="measure text-center">
<h2 class="h3">Come and see us</h2>
<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
+9 -9
View File
@@ -15,7 +15,7 @@
<!--/* w-full/min-w-0 keep this flex item from sizing to its max-content width and blowing out the
page on narrow screens. */-->
<div class="relative container mx-auto px-4 w-full min-w-0">
<div class="relative container w-full min-w-0">
<div class="max-w-3xl mx-auto text-center">
<div th:replace="~{fragments/lockup :: large('text-bakery-50 mb-10')}"></div>
<p class="lede sm:text-xl text-bakery-100 mb-10">
@@ -37,7 +37,7 @@
<!--/* What people come in for */-->
<section class="section bg-bakery-50">
<div class="container mx-auto px-4">
<div class="container">
<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 th:each="item : ${ {'Cinnamon Rolls', 'Sugar Cookies', 'Cakes'} }"
@@ -55,14 +55,14 @@
<!--/* Our story */-->
<section class="section bg-bakery-800 text-white">
<div class="container mx-auto px-4">
<div class="container">
<div class="max-w-3xl mx-auto text-center">
<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">
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.
</p>
<a href="/history" class="text-white hover:text-bakery-200 font-semibold underline underline-offset-4">
<a href="/history" class="link-on-dark font-semibold">
Read our story
</a>
</div>
@@ -75,11 +75,11 @@
the link is worth following.
*/-->
<section class="section bg-bakery-50">
<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="container">
<div class="grid md:grid-cols-2 gap-8 md:gap-12 items-center measure-wide">
<img th:src="${photos.of('gallery/Sugar Cookies.webp')}" alt="Iced sugar cookies from The Vine"
width="800" height="600" loading="lazy"
class="w-full h-64 md:h-80 object-cover rounded-panel shadow-xs">
class="photo w-full h-64 md:h-80 object-cover">
<div>
<h2 class="h2 text-bakery-900">Ordering for a crowd?</h2>
<p class="mt-4 text-bakery-800 leading-relaxed">
@@ -97,7 +97,7 @@
<!--/* Visit us */-->
<section id="visit" class="section bg-bakery-50 scroll-mt-24">
<div class="container mx-auto px-4">
<div class="container">
<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="panel p-6 md:p-8">
@@ -107,7 +107,7 @@
<div class="panel p-6 md:p-8">
<h2 class="h3 text-bakery-900 mb-6">Find us</h2>
<div class="text-bakery-800"
th:replace="~{fragments/visit :: address('hover:text-bakery-600 underline underline-offset-4')}"></div>
th:replace="~{fragments/visit :: address('link-plain')}"></div>
</div>
</div>
</div>
+6 -6
View File
@@ -3,15 +3,15 @@
<body>
<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="page-head">
<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 measure leading-relaxed">
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.
</p>
</header>
<div class="container mx-auto px-4 pb-16">
<div class="container pb-16">
<!--/*
The category filter: links, not buttons. Each filtered view is now a URL you can bookmark or send
to somebody, the browser's back button does what it looks like it does, and a crawler can see all
@@ -46,14 +46,14 @@
<p th:if="${#lists.isEmpty(products)}" class="text-center text-bakery-800">
Nothing in that category just now.
<a href="/products" class="underline underline-offset-4">See everything</a>.
<a href="/products" class="link-plain">See everything</a>.
</p>
</div>
<!--/* A catalogue with no way to order from it is a gallery. */-->
<section class="section bg-bakery-800 text-white">
<div class="container mx-auto px-4">
<div class="max-w-2xl mx-auto text-center">
<div class="container">
<div class="measure text-center">
<h2 class="h3">Want one of these, or something else?</h2>
<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