fix: brand contrast — chick-only header mark, ink callouts, nav polish

Phase 1 brand palette reused --c-sky for both the header background and
the word "Babies" inside the logo art, erasing it visually. Same class
of problem hit the contact page mailto callout.

- Split logo into chick mark + HTML site title so the wordmark colors
  no longer need to coexist with the header surface. Generator gains
  build_logo_mark_{png,webp} with a widest-gap column scan to crop.
- Header moves to --c-wheat; nav active state flips to ink pill with
  cream text; muted Shop link reads as coming-soon (italic + dim +
  not-allowed).
- Contact page mailto callout reskinned to ink/cream (strong CTA) and
  form note shifts from pale sky-deep to ink at 70% opacity.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-21 20:07:30 -05:00
parent cd87db8e07
commit f5098c05f5
5 changed files with 155 additions and 16 deletions

View File

@@ -158,8 +158,8 @@ a:focus-visible {
/* Header / brand / nav ---------------------------------------------------- */ /* Header / brand / nav ---------------------------------------------------- */
.site-header { .site-header {
background-color: var(--c-sky); background-color: var(--c-wheat);
border-bottom: 1px solid rgba(43, 58, 66, 0.1); border-bottom: 1px solid rgba(43, 58, 66, 0.15);
} }
.site-header__wrap { .site-header__wrap {
@@ -174,12 +174,30 @@ a:focus-visible {
.site-header__brand { .site-header__brand {
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
gap: var(--space-3);
text-decoration: none; text-decoration: none;
color: var(--c-ink);
} }
.site-header__logo { .site-header__mark {
height: 48px; height: 56px;
width: auto; width: auto;
flex-shrink: 0;
}
.site-header__title {
font-family: var(--font-serif);
font-weight: 700;
font-size: 1.5rem;
line-height: 1.1;
letter-spacing: 0.01em;
color: var(--c-ink);
}
@media (max-width: 30rem) {
.site-header__title {
font-size: 1.25rem;
}
} }
/* Mobile nav toggle (shown < 48rem, hidden ≥ 48rem). */ /* Mobile nav toggle (shown < 48rem, hidden ≥ 48rem). */
@@ -233,22 +251,37 @@ a:focus-visible {
text-decoration: none; text-decoration: none;
color: var(--c-ink); color: var(--c-ink);
font-weight: 600; font-weight: 600;
transition: background-color 120ms ease, color 120ms ease;
} }
.site-nav__link:hover, .site-nav__link:hover,
.site-nav__link:focus-visible { .site-nav__link:focus-visible {
background-color: rgba(255, 255, 255, 0.35); background-color: rgba(43, 58, 66, 0.08);
color: var(--c-ink); color: var(--c-ink);
} }
.site-nav__link.is-active { .site-nav__link.is-active {
background-color: var(--c-cream); background-color: var(--c-ink);
color: var(--c-sky-deep); color: var(--c-cream);
}
.site-nav__link.is-active:hover,
.site-nav__link.is-active:focus-visible {
background-color: var(--c-ink);
color: var(--c-cream);
} }
/* Muted link for not-yet-live destinations (Shop in Phase 1). */ /* Muted link for not-yet-live destinations (Shop in Phase 1). */
.site-nav__link.nav--muted { .site-nav__link.nav--muted {
opacity: 0.65; opacity: 0.55;
font-style: italic;
font-weight: 500;
}
.site-nav__link.nav--muted:hover,
.site-nav__link.nav--muted:focus-visible {
background-color: transparent;
cursor: not-allowed;
} }
/* Main + footer ----------------------------------------------------------- */ /* Main + footer ----------------------------------------------------------- */
@@ -387,16 +420,34 @@ a:focus-visible {
/* Contact form (inert in Phase 1). */ /* Contact form (inert in Phase 1). */
.contact-mailto { .contact-mailto {
background-color: var(--c-sky); background-color: var(--c-ink);
color: var(--c-cream);
border-radius: var(--radius); border-radius: var(--radius);
padding: var(--space-3); padding: var(--space-3);
} }
.contact-mailto a {
color: var(--c-cream);
text-decoration-color: rgba(250, 243, 231, 0.5);
}
.contact-mailto a:hover,
.contact-mailto a:focus-visible {
color: #ffffff;
text-decoration-color: currentColor;
}
.contact-mailto--muted { .contact-mailto--muted {
background-color: var(--c-wheat); background-color: var(--c-wheat);
color: var(--c-ink);
font-style: italic; font-style: italic;
} }
.contact-mailto--muted a {
color: var(--c-ink);
text-decoration-color: currentColor;
}
.contact-form { .contact-form {
display: grid; display: grid;
gap: var(--space-3); gap: var(--space-3);
@@ -407,7 +458,8 @@ a:focus-visible {
.contact-form__note { .contact-form__note {
margin-top: var(--space-3); margin-top: var(--space-3);
font-style: italic; font-style: italic;
color: var(--c-sky-deep); color: var(--c-ink);
opacity: 0.7;
} }
.contact-form__field { .contact-form__field {

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.1 KiB

View File

@@ -34,14 +34,17 @@
<header class="site-header"> <header class="site-header">
<div class="wrap site-header__wrap"> <div class="wrap site-header__wrap">
<a class="site-header__brand" href="/" aria-label="Chicken Babies R Us home"> <a class="site-header__brand" href="/" aria-label="Chicken Babies R Us home">
{# WebP with PNG fallback — generated by scripts/generate_static_assets.py. #} {# Chick-only mark paired with the site title as styled text.
Decoupling the mark from the wordmark lets the header colors
change freely without the multi-colored logo text clashing. #}
<picture> <picture>
<source srcset="{{ url_for('static', path='img/logo.webp') }}" type="image/webp"> <source srcset="{{ url_for('static', path='img/logo-mark.webp') }}" type="image/webp">
<img src="{{ url_for('static', path='img/logo.png') }}" <img src="{{ url_for('static', path='img/logo-mark.png') }}"
alt="Chicken Babies R Us" alt=""
height="48" height="56"
class="site-header__logo"> class="site-header__mark">
</picture> </picture>
<span class="site-header__title">Chicken Babies R Us</span>
</a> </a>
{# The mobile toggle button — script below attaches a click handler {# The mobile toggle button — script below attaches a click handler

View File

@@ -5,6 +5,8 @@ This script is the single source of truth for every image under
- ``logo.png`` — 256px tall, transparent RGBA - ``logo.png`` — 256px tall, transparent RGBA
- ``logo.webp`` — same size, WebP quality=82, method=6 - ``logo.webp`` — same size, WebP quality=82, method=6
- ``logo-mark.png`` — chick-only mark, 128px tall, transparent RGBA
- ``logo-mark.webp`` — same, WebP quality=82, method=6
- ``favicon.ico`` — multi-size 16/32/48 from a square crop - ``favicon.ico`` — multi-size 16/32/48 from a square crop
- ``apple-touch-icon.png`` — 180x180 with a cream background - ``apple-touch-icon.png`` — 180x180 with a cream background
@@ -42,6 +44,7 @@ _STATIC_IMG_DIR: Path = _REPO_ROOT / "app" / "static" / "img"
# Target output sizes. # Target output sizes.
_LOGO_TARGET_HEIGHT_PX: int = 256 # 2x the 48px display height in the header. _LOGO_TARGET_HEIGHT_PX: int = 256 # 2x the 48px display height in the header.
_LOGO_MARK_TARGET_HEIGHT_PX: int = 128 # ~2x the 56px header-icon display size.
_WEBP_QUALITY: int = 82 _WEBP_QUALITY: int = 82
_WEBP_METHOD: int = 6 # 0 = fastest, 6 = best compression. _WEBP_METHOD: int = 6 # 0 = fastest, 6 = best compression.
_FAVICON_SIZES: tuple[tuple[int, int], ...] = ((16, 16), (32, 32), (48, 48)) _FAVICON_SIZES: tuple[tuple[int, int], ...] = ((16, 16), (32, 32), (48, 48))
@@ -107,6 +110,29 @@ class StaticAssetBuilder:
) )
return out_path return out_path
def build_logo_mark_png(self) -> Path:
"""Write the chick-only mark as RGBA PNG."""
mark = self._aspect_resize(
self._crop_chick_mark(), height=_LOGO_MARK_TARGET_HEIGHT_PX
)
out_path = self._output_dir / "logo-mark.png"
mark.save(out_path, format="PNG", optimize=True)
return out_path
def build_logo_mark_webp(self) -> Path:
"""Write the chick-only mark as WebP."""
mark = self._aspect_resize(
self._crop_chick_mark(), height=_LOGO_MARK_TARGET_HEIGHT_PX
)
out_path = self._output_dir / "logo-mark.webp"
mark.save(
out_path,
format="WEBP",
quality=_WEBP_QUALITY,
method=_WEBP_METHOD,
)
return out_path
def build_favicon(self) -> Path: def build_favicon(self) -> Path:
"""Write the multi-size ICO favicon built from a square crop. """Write the multi-size ICO favicon built from a square crop.
@@ -149,6 +175,62 @@ class StaticAssetBuilder:
new_w = max(1, round(src_w * (height / src_h))) new_w = max(1, round(src_w * (height / src_h)))
return image.resize((new_w, height), resample=Image.Resampling.LANCZOS) return image.resize((new_w, height), resample=Image.Resampling.LANCZOS)
def _crop_chick_mark(self) -> Image.Image:
"""Return a copy of the source cropped to just the chick artwork.
Strategy: scan the source column-by-column for transparency,
find the widest contiguous transparent "gap" between opaque
columns, and treat the content to the left of that gap as the
chick. This is robust to future logo tweaks (new fonts, wider
tracking, shifted text) because it does not rely on hardcoded
pixel coordinates — only on the visual fact that the brand
mark and wordmark are separated by a wider gap than any gap
internal to either side.
"""
img = self._source
alpha = img.split()[-1]
width, height = img.size
# Per-column "has any opaque pixel" flags. getextrema on a 1px
# band returns (min, max); max > 0 means at least one opaque
# pixel. This is O(width) Pillow calls — fast enough here.
opaque = [
alpha.crop((x, 0, x + 1, height)).getextrema()[1] > 0
for x in range(width)
]
first_opaque = next((x for x, o in enumerate(opaque) if o), None)
last_opaque = next(
(x for x in range(width - 1, -1, -1) if opaque[x]), None
)
if first_opaque is None or last_opaque is None:
raise ValueError("Source logo has no opaque pixels.")
# Enumerate transparent runs strictly between first/last opaque.
gaps: list[tuple[int, int]] = [] # (gap_start, gap_end_exclusive)
run_start: int | None = None
for x in range(first_opaque, last_opaque + 1):
if not opaque[x] and run_start is None:
run_start = x
elif opaque[x] and run_start is not None:
gaps.append((run_start, x))
run_start = None
if not gaps:
raise ValueError(
"No transparent gap found between chick and wordmark; "
"cannot split the mark automatically."
)
gap_start, _ = max(gaps, key=lambda g: g[1] - g[0])
chick_slice = img.crop((first_opaque, 0, gap_start, height))
# Trim vertical whitespace so the resulting icon sits flush.
inner = chick_slice.getbbox()
if inner is None:
raise ValueError("Detected chick region is empty.")
return chick_slice.crop(inner)
@staticmethod @staticmethod
def _square_pad( def _square_pad(
image: Image.Image, image: Image.Image,
@@ -181,6 +263,8 @@ def main() -> int:
generated: list[Path] = [ generated: list[Path] = [
builder.build_logo_png(), builder.build_logo_png(),
builder.build_logo_webp(), builder.build_logo_webp(),
builder.build_logo_mark_png(),
builder.build_logo_mark_webp(),
builder.build_favicon(), builder.build_favicon(),
builder.build_apple_touch_icon(), builder.build_apple_touch_icon(),
] ]