Archived
Make the catering page look like it belongs to the shop
build-and-publish / build (pull_request) Successful in 2m7s
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:
@@ -1,121 +1,181 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" xmlns:th="http://www.thymeleaf.org" th:replace="~{fragments/page :: page(${title}, ${description}, ${path}, ~{::content})}">
|
||||
<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 & 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 — tell us what you have in mind and we will work from it.
|
||||
</p>
|
||||
</header>
|
||||
<!--/*
|
||||
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>
|
||||
|
||||
<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">
|
||||
<div class="relative container mx-auto px-4 w-full min-w-0">
|
||||
<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 & 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>
|
||||
|
||||
<!--/*
|
||||
One table per package. Two renderings of the same data, and only one is ever visible:
|
||||
<div class="bg-bakery-50">
|
||||
<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
|
||||
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 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">—</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">—</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 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
|
||||
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">–</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">–</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">→</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>
|
||||
|
||||
<!--/* 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>
|
||||
</html>
|
||||
|
||||
Reference in New Issue
Block a user