Make a card look like it came from the shop, not from a component library

The cards were white boxes with a grey drop shadow and no edge — the default card of every web app,
sitting on a page that is otherwise cream, sage and a hand-drawn vine. Four changes, all of them things
the brand already owns:

- A WARM HAIRLINE. A card now has an edge, so it reads as stock lying on a counter rather than as a panel
  floating over a screen.
- A SHADOW IN SAGE, not black — rgba(35,43,30,…), the darkest colour in the palette. Grey shadows are what
  made them feel borrowed.
- A CREAM HEAD (`panel-head`). On a catering card the size, price and "About 6–8 people" sit on the tint
  with the contents below, the way a printed menu separates what it costs from what you get. Product cards
  use the same band for their caption, so the photo reads as mounted on the card instead of being it.
- THE BRANCH, at watermark weight, in the corner of every price card. It is the one mark that is
  unmistakably this shop, and it costs nothing: the mask was already there for the wordmark.

The lift is a lift now, two pixels, rather than the shadow just darkening.

TWO THINGS TAILWIND v4 MADE ME FIX ALONG THE WAY. `.mark`, `.mark-l`, `.mark-r` and `.no-scrollbar` were
plain classes in @layer components, and only a registered utility can be @applied by another rule — the
watermark builds on the branch mark, so they became @utility like everything else here. And rewriting that
file dropped the `summary { list-style: none }` reset, which would have put a disclosure triangle beside
the hamburger on every phone; caught it by grepping the compiled CSS rather than by luck.

53 tests green, every class on every page resolves, and the price cards are in the screenshot.
This commit is contained in:
2026-07-26 18:29:05 -05:00
parent ec61b40b12
commit a42f1ee8b1
6 changed files with 112 additions and 65 deletions
+15 -10
View File
@@ -67,19 +67,24 @@
: (${#lists.size(table.tiers) == 2} ? 'sm:grid-cols-2 max-w-3xl mx-auto'
: 'sm:grid-cols-2 lg:grid-cols-3')">
<article th:each="tier, t : ${table.tiers}"
class="flex flex-col panel-lift p-6 md:p-8">
<p class="label 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>
<!--/* No price means "ask us" — say so rather than leaving a hole in the card. */-->
<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
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>
class="relative flex flex-col overflow-hidden panel-lift">
<!--/* The wordmark's branch, at watermark weight. */-->
<span class="panel-mark -top-3 -right-4 w-24 h-24 rotate-12" aria-hidden="true"></span>
<ul class="mt-6 space-y-3 text-bakery-800">
<div class="panel-head relative px-6 py-6 md:px-8">
<p class="label 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>
<!--/* No price means "ask us" — say so rather than leaving a hole in the card. */-->
<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
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>
</div>
<ul class="px-6 py-6 md:px-8 space-y-3 text-bakery-800">
<li th:each="row : ${table.rows}" class="flex gap-3">
<!--/* A tick, not a bullet: this is a list of what you get, not prose. */-->
<svg class="mt-1 h-4 w-4 shrink-0 text-bakery-400" viewBox="0 0 24 24" fill="none"
<svg class="mt-1 h-4 w-4 shrink-0 text-bakery-500" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
aria-hidden="true">
<path d="M20 6L9 17l-5-5" />
+1 -1
View File
@@ -47,7 +47,7 @@
<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">
</div>
<h3 class="h4 md:text-2xl text-bakery-800 py-6" th:text="${item}">Cinnamon Rolls</h3>
<h3 class="panel-head border-b-0 border-t h4 md:text-2xl text-bakery-800 py-6" th:text="${item}">Cinnamon Rolls</h3>
</div>
</div>
</div>
+4 -2
View File
@@ -35,8 +35,10 @@
<div class="relative w-full overflow-hidden">
<div th:replace="~{:: gallery(${product})}"></div>
</div>
<div class="p-6 text-center">
<h3 class="h4 text-bakery-900 mb-2" th:text="${product.name}">Name</h3>
<!--/* The caption sits on the cream, so the photo reads as mounted on the card rather than as
the card itself. */-->
<div class="panel-head border-b-0 border-t px-6 py-5 text-center">
<h3 class="h4 text-bakery-900" th:text="${product.name}">Name</h3>
<span class="label text-bakery-600" th:text="${product.category}">Category</span>
</div>
</div>