Merge pull request 'Hero title: trial Eileen Caps Black, larger, stacked on phones' (#4) from feat/hero/eileen-caps-title into main

Reviewed-on: #4
This commit is contained in:
padreug 2026-09-20 21:08:36 +00:00
commit 1ced09e728
3 changed files with 63 additions and 9 deletions

Binary file not shown.

View file

@ -16,6 +16,17 @@
font-display: swap; font-display: swap;
} }
/* Trial hero-title face: "Eileen Caps Black" (D. Rakowski, freeware via
fontspace.com/eileen-caps-font-f2027). Caps-only-ish display face with no
circumflex glyph, so the hero text drops the accent on "Château". */
@font-face {
font-family: 'Eileen Caps Black';
src: url('./assets/fonts/eileen-caps-black.woff2') format('woff2');
font-weight: normal;
font-style: normal;
font-display: swap;
}
:root { :root {
/* Brand palette: deep forest green dominant background, cream /* Brand palette: deep forest green dominant background, cream
foreground, gold (Aboreto/calligraphic spirit) for CTAs and accents. */ foreground, gold (Aboreto/calligraphic spirit) for CTAs and accents. */
@ -99,7 +110,7 @@
'Roboto', ui-sans-serif, system-ui, -apple-system, 'Segoe UI', sans-serif; 'Roboto', ui-sans-serif, system-ui, -apple-system, 'Segoe UI', sans-serif;
--font-display: 'Aboreto', 'Cormorant Garamond', Georgia, serif; --font-display: 'Aboreto', 'Cormorant Garamond', Georgia, serif;
--font-script: 'Tangerine', 'Allura', cursive; --font-script: 'Tangerine', 'Allura', cursive;
--font-title: 'Berry Rotunda', 'UnifrakturMaguntia', serif; --font-title: 'Eileen Caps Black', 'Berry Rotunda', 'UnifrakturMaguntia', serif;
} }
@layer base { @layer base {

View file

@ -1,5 +1,5 @@
<script setup lang="ts"> <script setup lang="ts">
import { computed, nextTick, onMounted, ref } from 'vue' import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import { RouterLink } from 'vue-router' import { RouterLink } from 'vue-router'
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
@ -21,7 +21,7 @@ const tileStyle = {
const { t } = useI18n() const { t } = useI18n()
// The hero title is SVG text (Berry Rotunda) used as a clip over the // The hero title is SVG text (Eileen Caps Black) used as a clip over the
// wordmark video. Measure the rendered glyph box once the font loads and fit // wordmark video. Measure the rendered glyph box once the font loads and fit
// the viewBox/foreignObject to it, so the wordmark fills the SVG width without // the viewBox/foreignObject to it, so the wordmark fills the SVG width without
// clipping regardless of the font's metrics. // clipping regardless of the font's metrics.
@ -32,7 +32,23 @@ const titleViewBox = computed(
() => `${titleBox.value.x} ${titleBox.value.y} ${titleBox.value.w} ${titleBox.value.h}`, () => `${titleBox.value.x} ${titleBox.value.y} ${titleBox.value.w} ${titleBox.value.h}`,
) )
onMounted(async () => { // Below the `sm` breakpoint the three words stack one per line (the wide
// caps face otherwise renders as a thin strip on phones); from sm up they sit
// on one line. Re-measure whenever the layout flips.
const titleWords = ['Chateau', 'du', 'Faune']
// Stacked layout: "du" at half size. Baseline offsets are absolute user
// units (not em, which would scale with each line's own size) chosen so the
// gap above and below the small line is the same.
const titleLineSize = [120, 60, 120]
const titleLineDy = [0, 83, 133]
const stackedQuery = '(max-width: 639px)'
const titleStacked = ref(false)
let stackedMql: MediaQueryList | null = null
const onStackedChange = (e: MediaQueryListEvent) => {
titleStacked.value = e.matches
}
async function measureTitle() {
try { try {
await (document as Document & { fonts?: FontFaceSet }).fonts?.ready await (document as Document & { fonts?: FontFaceSet }).fonts?.ready
} catch { } catch {
@ -48,14 +64,30 @@ onMounted(async () => {
return return
} }
if (!bb.width) return if (!bb.width) return
const padX = bb.height * 0.08 const lineH = bb.height / (titleStacked.value ? titleWords.length : 1)
const padY = bb.height * 0.16 const padX = lineH * 0.08
const padY = lineH * 0.16
titleBox.value = { titleBox.value = {
x: Math.round(bb.x - padX), x: Math.round(bb.x - padX),
y: Math.round(bb.y - padY), y: Math.round(bb.y - padY),
w: Math.round(bb.width + padX * 2), w: Math.round(bb.width + padX * 2),
h: Math.round(bb.height + padY * 2), h: Math.round(bb.height + padY * 2),
} }
}
onMounted(() => {
stackedMql = window.matchMedia(stackedQuery)
titleStacked.value = stackedMql.matches
stackedMql.addEventListener('change', onStackedChange)
measureTitle()
})
onBeforeUnmount(() => {
stackedMql?.removeEventListener('change', onStackedChange)
})
watch(titleStacked, () => {
measureTitle()
}) })
const pathways = [ const pathways = [
@ -116,7 +148,7 @@ const featuredEvents = [
(always one line); a gold drop-shadow gives the engraved edge. --> (always one line); a gold drop-shadow gives the engraved edge. -->
<svg <svg
ref="titleSvg" ref="titleSvg"
class="hero-title -mt-1 block w-[min(92vw,52rem)] sm:mt-4" class="hero-title -mt-1 block w-full max-w-[68rem] sm:mt-4"
:viewBox="titleViewBox" :viewBox="titleViewBox"
aria-hidden="true" aria-hidden="true"
> >
@ -127,10 +159,21 @@ const featuredEvents = [
x="550" x="550"
y="122" y="122"
text-anchor="middle" text-anchor="middle"
font-family="'Berry Rotunda', 'UnifrakturMaguntia', serif" font-family="'Eileen Caps Black', 'Berry Rotunda', 'UnifrakturMaguntia', serif"
font-size="120" font-size="120"
> >
Château du Faune <template v-if="titleStacked">
<tspan
v-for="(word, i) in titleWords"
:key="word"
x="550"
:dy="titleLineDy[i]"
:font-size="titleLineSize[i]"
>
{{ word }}
</tspan>
</template>
<template v-else>{{ titleWords.join(' ') }}</template>
</text> </text>
</clipPath> </clipPath>
</defs> </defs>