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:
@@ -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 {
|
||||||
|
|||||||
BIN
app/static/img/logo-mark.png
Normal file
BIN
app/static/img/logo-mark.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 16 KiB |
BIN
app/static/img/logo-mark.webp
Normal file
BIN
app/static/img/logo-mark.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 6.1 KiB |
@@ -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
|
||||||
|
|||||||
@@ -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(),
|
||||||
]
|
]
|
||||||
|
|||||||
Reference in New Issue
Block a user