Make the catering page look like it belongs to the shop

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.
This commit is contained in:
2026-07-26 17:03:06 -05:00
parent faf7bd9f18
commit ad8ad724a3
+168 -108
View File
@@ -1,121 +1,181 @@
<!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> A short photographic band rather than the plain cream header the other pages use. This is the one
<p class="mt-4 text-bakery-800 max-w-2xl mx-auto leading-relaxed"> page that is selling something, and the same treatment as the homepage hero — blurred photo, sage
Boxes for the office, packages for a party, and cakes and desserts for a wedding. Every one of wash — carries the brand rather than inventing a new look for it.
these is a starting point &mdash; tell us what you have in mind and we will work from it. */-->
</p> <section class="relative flex items-center py-14 md:py-20 bg-bakery-900 text-white overflow-hidden">
</header> <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>
<div class="container mx-auto px-4 pb-16 md:pb-24"> <div class="relative container mx-auto px-4 w-full min-w-0">
<div class="max-w-4xl mx-auto space-y-10 md:space-y-12"> <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">
One table per package. Two renderings of the same data, and only one is ever visible: <div class="container mx-auto px-4 pt-14 pb-16 md:pt-16 md:pb-24">
- A real <table> from `md` up. These are prices in columns; a table is what that is, and a <p class="max-w-2xl mx-auto text-center text-bakery-800 leading-relaxed">
screen reader announces the size and the item together because of the row and column headers. Every one of these is a starting point. Tell us the date, the number of people and what you had in
- Stacked cards below `md`. A four-column price table on a phone is either an illegible squeeze mind, and we will work from it.
or a sideways scroll, and this page is mostly read on phones.
Both come from the same model, so they cannot drift.
*/-->
<section th:each="table : ${menu.packages}"
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. */-->
<div class="hidden md:block px-8 pt-6">
<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>
<thead>
<tr class="border-b border-bakery-200">
<th scope="col" class="py-3 pr-4 text-xs uppercase tracking-[0.15em] text-bakery-600 font-medium align-bottom">
What you get
</th>
<th th:each="tier : ${table.tiers}" scope="col" class="py-3 px-4 align-bottom">
<span class="block font-adbhashitha text-lg text-bakery-900" th:text="${tier.label}">Small</span>
<span th:if="${tier.price}" class="block text-bakery-700" th:text="${tier.price}">$24</span>
<!--/* 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>
</th>
</tr>
</thead>
<tbody>
<tr th:each="row : ${table.rows}" class="border-b border-bakery-100 last:border-0 align-top">
<th scope="row" class="py-4 pr-4 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">
<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
cell reads as a broken page. */-->
<span th:if="${#strings.isEmpty(value)}" class="text-bakery-400" aria-hidden="true">&mdash;</span>
</td>
</tr>
</tbody>
</table>
</div>
<!--/* Narrow: one card per size. */-->
<div class="md:hidden px-6 pt-6 space-y-4">
<div th:each="tier, t : ${table.tiers}" class="rounded-2xl border border-bakery-200 p-4">
<div class="flex items-baseline justify-between gap-3">
<h3 class="font-adbhashitha text-lg text-bakery-900" 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:unless="${tier.price}" class="text-bakery-600 text-sm whitespace-nowrap">Ask us</span>
</div>
<!--/* 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
still listed: they are part of what's in the box, and the bakery just hasn't said how
many yet. */-->
<dl class="mt-3 space-y-2">
<div th:each="row : ${table.rows}" class="flex justify-between gap-4 text-sm">
<dt class="text-bakery-900" th:text="${row.label}">Mini muffins</dt>
<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])}" class="text-bakery-400">&mdash;</span>
</dd>
</div>
</dl>
</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">
<li th:each="note : ${table.notes}" th:text="${note}">Minimum of 6 items per baked good.</li>
</ul>
</div>
<div class="px-6 pb-6 pt-6 md:px-8 md:pb-8">
<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">
Ask about <span th:text="${#strings.toLowerCase(table.name)}">office</span>
</a>
</div>
</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. */-->
<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
talk it through.
</p> </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
or a sideways scroll, and this page is mostly read on phones.
Both walk the same rows, so they cannot drift.
*/-->
<section th:each="table, tbl : ${menu.packages}"
class="bg-white rounded-3xl shadow-xs overflow-hidden">
<header class="px-6 pt-7 pb-5 md:px-10 md:pt-8 text-center border-b border-bakery-100">
<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">
<caption class="sr-only" th:text="|${table.name} — what each size includes and what it costs|">Sizes</caption>
<thead>
<tr>
<!--/* A blank corner: the caption above says what the table is, and every row names
itself, so a label here only competed with the prices. */-->
<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-6 px-4 text-center align-bottom border-b-2 border-bakery-200">
<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. */-->
<span th:unless="${tier.price}" class="mt-2 block font-adbhashitha text-xl text-bakery-700">Ask us</span>
</th>
</tr>
</thead>
<tbody>
<tr th:each="row : ${table.rows}" class="align-top even:bg-bakery-50/60">
<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-5 px-4 text-center text-bakery-800">
<span th:if="${!#strings.isEmpty(value)}" th:text="${value}">12 items</span>
<!--/* A cell the bakery hasn't filled in. A dash reads as "nothing here"; an empty cell
reads as a broken page. */-->
<span th:if="${#strings.isEmpty(value)}" class="text-bakery-400" aria-hidden="true">&ndash;</span>
</td>
</tr>
</tbody>
</table>
</div>
<!--/* Narrow: one card per size. */-->
<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 overflow-hidden">
<div class="flex items-baseline justify-between gap-3 bg-bakery-50 px-4 py-3">
<h3 class="text-xs uppercase tracking-[0.15em] text-bakery-600" th:text="${tier.label}">Small</h3>
<span th:if="${tier.price}" class="font-adbhashitha text-2xl text-bakery-900 whitespace-nowrap"
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>
<!--/* 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
still listed: they are part of what's in the box, and the bakery just hasn't said how
many yet. */-->
<dl class="divide-y divide-bakery-100">
<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>
<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])}" class="text-bakery-400">&ndash;</span>
</dd>
</div>
</dl>
</div>
</div>
<!--/*
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>
</ul>
</div>
<!--/* One text run, so the underline is continuous: as flex children with a gap it came out
as "Ask about" and "office" separately underlined. The arrow stays outside it. */-->
<a th:href="|/contact?about=${#uris.escapeQueryParam(table.name)}|"
class="shrink-0 text-bakery-700 hover:text-bakery-900 font-medium transition-colors whitespace-nowrap">
<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>
</div>
</section>
<!--/* 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
talk it through.
</p>
</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> </div>
</body> </body>
</html> </html>