Make the catering page look like it belongs to the shop
build-and-publish / build (pull_request) Successful in 2m7s

It was correct and plain: a heading, three white cards, a bare table in each. Now it reads like the rest
of the site, using only what the site already has.

- A short photographic band, same treatment as the homepage hero — blurred cake photo, sage wash, an
  eyebrow line above the heading. Short: the content is three lines, and 34svh of dark left the title
  floating in a void.
- Prices are the loudest thing in each table, set in the display serif above a small-caps size label,
  over a ruled header band. Zebra rows below, the line names in the left column.
- The branch from the wordmark divides the intro from the tables — it is a mask over currentColor, so
  it takes the colour of the text around it and costs one span.
- Small print and the enquiry link share one tinted card footer. As separate full-width bands they made
  every card read as four stacked stripes. The link is quiet: three filled buttons down a page shout,
  and the real call to action belongs once, in a closing sage band with the terms and the phone number.
- The blank-cell dash was so faint that a line with no quantities looked like a rendering fault.

Checked by rendering it, at 1280 and at 390: hero, tables, the phone's one-card-per-size stack, the
closing band. All 197 classes the page uses resolve in the compiled stylesheet, so nothing is styled by
accident. Two notes for anyone doing the same: headless Chromium screenshots this site mid-fade unless
you pass --virtual-time-budget (the body fades in), and a very tall window makes min-h-screen stretch
the flex column, which looks like a gap above the footer and is not one.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
This commit is contained in:
2026-07-26 17:03:06 -05:00
co-authored by Claude Opus 5
parent 4ac984b33d
commit 8ab302dd19
+120 -60
View File
@@ -1,61 +1,91 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org" th:replace="~{fragments/page :: page(${title}, ${description}, ${path}, ~{::content})}"> <html lang="en" xmlns:th="http://www.thymeleaf.org" th:replace="~{fragments/page :: page(${title}, ${description}, ${path}, ~{::content})}">
<body> <body>
<div th:fragment="content" class="min-h-screen bg-bakery-50"> <div th:fragment="content">
<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">Goodie boxes &amp; catering</h1>
<p class="mt-4 text-bakery-800 max-w-2xl mx-auto leading-relaxed">
Boxes for the office, packages for a party, and cakes and desserts for a wedding. Every one of
these is a starting point &mdash; tell us what you have in mind and we will work from it.
</p>
</header>
<div class="container mx-auto px-4 pb-16 md:pb-24">
<div class="max-w-4xl mx-auto space-y-10 md:space-y-12">
<!--/* <!--/*
One table per package. Two renderings of the same data, and only one is ever visible: A short photographic band rather than the plain cream header the other pages use. This is the one
page 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.
*/-->
<section class="relative flex items-center py-14 md:py-20 bg-bakery-900 text-white overflow-hidden">
<img th:src="${photos.of('gallery/Cakes.webp')}" alt=""
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-gradient-to-t from-bakery-900 via-bakery-800/60 to-bakery-900/80"></div>
- A real <table> from `md` up. These are prices in columns; a table is what that is, and a <div class="relative container mx-auto px-4 w-full min-w-0">
screen reader announces the size and the item together because of the row and column headers. <div class="max-w-3xl mx-auto text-center">
<p class="text-xs sm:text-sm uppercase tracking-[0.2em] text-bakery-200">To take away</p>
<h1 class="mt-3 font-adbhashitha text-4xl md:text-5xl text-bakery-50">Goodie boxes &amp; catering</h1>
<p class="mt-5 text-base sm:text-lg text-bakery-100 text-balance leading-relaxed">
Boxes for the office, packages for a party, and cakes and desserts for a wedding.
</p>
</div>
</div>
</section>
<div class="bg-bakery-50">
<div class="container mx-auto px-4 pt-14 pb-16 md:pt-16 md:pb-24">
<p class="max-w-2xl mx-auto 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 work from it.
</p>
<!--/* The branch from the wordmark, as a divider. It is a mask over currentColor, so it takes the
colour of the text around it — see .mark in tokens.css. */-->
<div class="mt-10 flex justify-center" aria-hidden="true">
<span class="mark mark-r w-12 h-12 text-bakery-400"></span>
</div>
<div class="mt-10 max-w-5xl mx-auto space-y-10 md:space-y-14">
<!--/*
One card per table, and each table is rendered twice from the same model with only one visible:
- A real <table> from `md` up. These are prices in columns; a table is what that is, and a screen
reader announces the size and the item together because of the row and column headers.
- Stacked cards below `md`. A four-column price table on a phone is either an illegible squeeze - Stacked cards below `md`. A four-column price table on a phone is either an illegible squeeze
or a sideways scroll, and this page is mostly read on phones. or a sideways scroll, and this page is mostly read on phones.
Both come from the same model, so they cannot drift. Both walk the same rows, so they cannot drift.
*/--> */-->
<section th:each="table : ${menu.packages}" <section th:each="table, tbl : ${menu.packages}"
class="bg-white rounded-3xl shadow-xs overflow-hidden"> class="bg-white rounded-3xl shadow-xs overflow-hidden">
<div class="px-6 pt-6 md:px-8 md:pt-8">
<h2 class="font-adbhashitha text-2xl md:text-3xl text-bakery-900" th:text="${table.name}">Office</h2>
<p th:if="${table.blurb}" class="mt-2 text-bakery-700" th:text="${table.blurb}">Blurb</p>
</div>
<!--/* Wide: the table. */--> <header class="px-6 pt-7 pb-5 md:px-10 md:pt-8 text-center border-b border-bakery-100">
<div class="hidden md:block px-8 pt-6"> <h2 class="font-adbhashitha text-2xl md:text-3xl text-bakery-900" th:text="${table.name}">Office</h2>
<p th:if="${table.blurb}" class="mt-2 text-bakery-700 max-w-xl mx-auto" th:text="${table.blurb}">Blurb</p>
</header>
<!--/* Wide: the table. The header band carries the price; the rows are what you get for it. */-->
<div class="hidden md:block px-6 lg:px-10 py-2">
<table class="w-full border-collapse text-left"> <table class="w-full border-collapse text-left">
<caption class="sr-only" th:text="|${table.name} — what each size includes and what it costs|">Sizes</caption> <caption class="sr-only" th:text="|${table.name} — what each size includes and what it costs|">Sizes</caption>
<thead> <thead>
<tr class="border-b border-bakery-200"> <tr>
<th scope="col" class="py-3 pr-4 text-xs uppercase tracking-[0.15em] text-bakery-600 font-medium align-bottom"> <!--/* A blank corner: the caption above says what the table is, and every row names
What you get itself, so a label here only competed with the prices. */-->
</th> <th scope="col" class="w-52 py-5 pr-6"><span class="sr-only">What you get</span></th>
<th th:each="tier : ${table.tiers}" scope="col" class="py-3 px-4 align-bottom"> <th th:each="tier : ${table.tiers}" scope="col"
<span class="block font-adbhashitha text-lg text-bakery-900" th:text="${tier.label}">Small</span> class="py-6 px-4 text-center align-bottom border-b-2 border-bakery-200">
<span th:if="${tier.price}" class="block text-bakery-700" th:text="${tier.price}">$24</span> <span class="block text-xs uppercase tracking-[0.15em] text-bakery-600" th:text="${tier.label}">Small</span>
<span th:if="${tier.price}" class="mt-2 block font-adbhashitha text-3xl text-bakery-900"
th:text="${tier.price}">$24</span>
<!--/* No price means "ask us" — say so rather than leaving a hole in the column. */--> <!--/* No price means "ask us" — say so rather than leaving a hole in the column. */-->
<span th:unless="${tier.price}" class="block text-bakery-600 text-sm">Ask us</span> <span th:unless="${tier.price}" class="mt-2 block font-adbhashitha text-xl text-bakery-700">Ask us</span>
</th> </th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<tr th:each="row : ${table.rows}" class="border-b border-bakery-100 last:border-0 align-top"> <tr th:each="row : ${table.rows}" class="align-top even:bg-bakery-50/60">
<th scope="row" class="py-4 pr-4 font-medium text-bakery-900" th:text="${row.label}">Mini muffins</th> <th scope="row" class="py-5 pr-6 pl-2 font-medium text-bakery-900" th:text="${row.label}">Mini muffins</th>
<td th:each="value : ${row.values}" class="py-4 px-4 text-bakery-800"> <td th:each="value : ${row.values}" class="py-5 px-4 text-center text-bakery-800">
<span th:if="${!#strings.isEmpty(value)}" th:text="${value}">12 items</span> <span th:if="${!#strings.isEmpty(value)}" th:text="${value}">12 items</span>
<!--/* A cell the bakery hasn't filled in. An em dash reads as "nothing here"; an empty <!--/* A cell the bakery hasn't filled in. A dash reads as "nothing here"; an empty cell
cell reads as a broken page. */--> reads as a broken page. */-->
<span th:if="${#strings.isEmpty(value)}" class="text-bakery-400" aria-hidden="true">&mdash;</span> <span th:if="${#strings.isEmpty(value)}" class="text-bakery-400" aria-hidden="true">&ndash;</span>
</td> </td>
</tr> </tr>
</tbody> </tbody>
@@ -63,51 +93,56 @@
</div> </div>
<!--/* Narrow: one card per size. */--> <!--/* Narrow: one card per size. */-->
<div class="md:hidden px-6 pt-6 space-y-4"> <div class="md:hidden px-5 py-6 space-y-4">
<div th:each="tier, t : ${table.tiers}" class="rounded-2xl border border-bakery-200 p-4"> <div th:each="tier, t : ${table.tiers}" class="rounded-2xl border border-bakery-200 overflow-hidden">
<div class="flex items-baseline justify-between gap-3"> <div class="flex items-baseline justify-between gap-3 bg-bakery-50 px-4 py-3">
<h3 class="font-adbhashitha text-lg text-bakery-900" th:text="${tier.label}">Small</h3> <h3 class="text-xs uppercase tracking-[0.15em] text-bakery-600" th:text="${tier.label}">Small</h3>
<span th:if="${tier.price}" class="text-bakery-700 whitespace-nowrap" th:text="${tier.price}">$24</span> <span th:if="${tier.price}" class="font-adbhashitha text-2xl text-bakery-900 whitespace-nowrap"
<span th:unless="${tier.price}" class="text-bakery-600 text-sm whitespace-nowrap">Ask us</span> th:text="${tier.price}">$24</span>
<span th:unless="${tier.price}" class="font-adbhashitha text-lg text-bakery-700 whitespace-nowrap">Ask us</span>
</div> </div>
<!--/* The cell for this size on each line — t.index picks this column out of every row, which <!--/* The cell for this size on each line — t.index picks this column out of every row, which
is exactly the alignment the aggregate guarantees. Lines with nothing in this column are is exactly the alignment the aggregate guarantees. Lines with nothing in this column are
still listed: they are part of what's in the box, and the bakery just hasn't said how still listed: they are part of what's in the box, and the bakery just hasn't said how
many yet. */--> many yet. */-->
<dl class="mt-3 space-y-2"> <dl class="divide-y divide-bakery-100">
<div th:each="row : ${table.rows}" class="flex justify-between gap-4 text-sm"> <div th:each="row : ${table.rows}" class="flex justify-between gap-4 px-4 py-2.5 text-sm">
<dt class="text-bakery-900" th:text="${row.label}">Mini muffins</dt> <dt class="text-bakery-900" th:text="${row.label}">Mini muffins</dt>
<dd class="text-bakery-800 text-right"> <dd class="text-bakery-800 text-right">
<span th:if="${!#strings.isEmpty(row.values[t.index])}" th:text="${row.values[t.index]}">12 items</span> <span th:if="${!#strings.isEmpty(row.values[t.index])}" th:text="${row.values[t.index]}">12 items</span>
<span th:if="${#strings.isEmpty(row.values[t.index])}" class="text-bakery-400">&mdash;</span> <span th:if="${#strings.isEmpty(row.values[t.index])}" class="text-bakery-400">&ndash;</span>
</dd> </dd>
</div> </div>
</dl> </dl>
</div> </div>
</div> </div>
<div th:if="${!#lists.isEmpty(table.notes)}" class="px-6 md:px-8 pt-6"> <!--/*
<ul class="space-y-2 text-sm text-bakery-700"> The small print and the enquiry link share one tinted footer. As two more full-width bands
they made every card read as four stacked stripes.
The link is quiet rather than a filled button: three filled buttons down a page would shout,
and the loud call to action belongs once, at the end.
*/-->
<div class="px-6 md:px-10 py-6 bg-bakery-50/70 border-t border-bakery-100
flex flex-col gap-5 md:flex-row md:items-end md:justify-between">
<div th:if="${!#lists.isEmpty(table.notes)}" class="md:max-w-2xl">
<h3 class="text-xs uppercase tracking-[0.15em] text-bakery-500 font-medium">Good to know</h3>
<ul class="mt-3 space-y-2 text-sm text-bakery-700 list-disc pl-5 marker:text-bakery-300">
<li th:each="note : ${table.notes}" th:text="${note}">Minimum of 6 items per baked good.</li> <li th:each="note : ${table.notes}" th:text="${note}">Minimum of 6 items per baked good.</li>
</ul> </ul>
</div> </div>
<!--/* One text run, so the underline is continuous: as flex children with a gap it came out
<div class="px-6 pb-6 pt-6 md:px-8 md:pb-8"> as "Ask about" and "office" separately underlined. The arrow stays outside it. */-->
<a th:href="|/contact?about=${#uris.escapeQueryParam(table.name)}|" <a th:href="|/contact?about=${#uris.escapeQueryParam(table.name)}|"
class="inline-block bg-bakery-600 hover:bg-bakery-700 text-white px-6 py-3 rounded-full font-medium tracking-wide transition"> class="shrink-0 text-bakery-700 hover:text-bakery-900 font-medium transition-colors whitespace-nowrap">
Ask about <span th:text="${#strings.toLowerCase(table.name)}">office</span> <span class="underline underline-offset-4"
th:text="|Ask about ${#strings.toLowerCase(table.name)}|">Ask about office</span>
<span aria-hidden="true">&rarr;</span>
</a> </a>
</div> </div>
</section> </section>
<!--/* The terms that apply whichever table you were reading. */-->
<section th:if="${!#lists.isEmpty(menu.notes)}" class="rounded-3xl border border-bakery-200 p-6 md:p-8">
<h2 class="font-adbhashitha text-xl text-bakery-900">Before you order</h2>
<ul class="mt-4 space-y-2 text-bakery-800">
<li th:each="note : ${menu.notes}" th:text="${note}">Prices may change.</li>
</ul>
</section>
<!--/* Only shows if every table has been emptied out in the admin. Better than a bare heading. */--> <!--/* 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"> <p th:if="${#lists.isEmpty(menu.packages)}" class="text-center text-bakery-800">
Our catering list is being updated. Call us on Our catering list is being updated. Call us on
@@ -117,5 +152,30 @@
</div> </div>
</div> </div>
</div> </div>
<!--/* 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="bg-bakery-800 text-white py-16 md:py-20">
<div class="container mx-auto px-4">
<div class="max-w-2xl mx-auto text-center">
<h2 class="font-adbhashitha text-2xl md:text-3xl">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}">Prices may change.</li>
</ul>
<div class="mt-10 flex flex-col sm:flex-row gap-3 justify-center">
<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">
Tell us what you need
</a>
<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">
(309) 701-0660
</a>
</div>
</div>
</div>
</section>
</div>
</body> </body>
</html> </html>