diff --git a/resources/views/email.phtml b/resources/views/email.phtml index a8bcb17..cfaa2cb 100644 --- a/resources/views/email.phtml +++ b/resources/views/email.phtml @@ -24,27 +24,38 @@ use Illuminate\Support\Collection; * @var string $account_url */ -// ─── Aesthetic constants ──────────────────────────────────────────────── +// ─── BockenTheme light-mode palette ───────────────────────────────────── +// Pulled from src/scss/theme.scss + config/_theme-variables.scss in the +// Bocken theme so the newsletter and the website read as one product. $palette = [ - 'paper' => '#f8f1e3', // warm ivory background - 'ink' => '#3a2820', // deep oxblood-brown body text - 'gold' => '#a17536', // aged-gold accents - 'rule' => '#d4be91', // hairline rule - 'mute' => '#7a6a5e', // muted secondary text - 'shadow' => '#5a4a3e', // for darker symbols (death) + 'bg' => '#f8f6f1', // body / page background (--color-bg-primary) + 'surface' => '#efecea', // raised section cards (--color-bg-secondary) + 'elevated' => '#dfdcd8', // tertiary surface (--color-bg-elevated) + 'border' => '#ddd', // hairline borders (--color-border) + 'ink' => '#2a2a2a', // primary text (--color-text-primary) + 'ink2' => '#555', // secondary text (--color-text-secondary) + 'ink3' => '#777', // tertiary text (--color-text-tertiary) + 'mute' => '#aaa', // muted text (--color-text-muted) + 'link' => '#5E81AC', // nord10 — primary link + 'link_hov' => '#81A1C1', // nord9 + 'accent' => '#BF616A', // nord11 — red FAB accent + 'birth' => '#5E81AC', // nord10 — birth (blue) + 'death' => '#4C566A', // nord3 — death (graphite) + 'marr' => '#B48EAD', // nord15 — marriage (purple/pink) ]; -$serif_display = "'EB Garamond', Georgia, 'Times New Roman', serif"; -$serif_body = "Georgia, 'Iowan Old Style', 'Palatino Linotype', serif"; +$font_stack = "'Open Sans', Helvetica, Arial, 'Noto Sans', sans-serif"; $avatar_size = 56; // ─── Helpers ──────────────────────────────────────────────────────────── -$linked_name = static function (Individual $individual): string { +$linked_name = static function (Individual $individual) use ($palette): string { $name = strip_tags($individual->fullName()); $url = $individual->url(); - $style = 'color:inherit;text-decoration:none;border-bottom:1px solid #c8a96a;'; + $style = 'color:' . $palette['ink'] . ';text-decoration:none;' + . 'border-bottom:1px solid ' . $palette['border'] . ';' + . 'padding-bottom:1px;'; return '' . e($name) . ''; }; @@ -82,38 +93,35 @@ $event_date_display = static function (Fact $fact): string { }; $event_kind = static function (Fact $fact): string { - $tag = $fact->tag(); - $parts = explode(':', $tag); + $parts = explode(':', $fact->tag()); return end($parts); }; /** - * Compact inline SVG glyph for each event type. Rendered in modern - * webmail (Gmail web, Apple Mail, iOS, Outlook 365); Outlook desktop - * strips SVG silently — the textual event label still carries the - * meaning, so the email reads correctly without it. + * Compact inline SVG glyph for each event type. Coloured to match the + * BockenTheme Nord palette: birth in nord10 blue, death in nord3 + * graphite, marriage in nord15 mauve. */ $event_icon = static function (string $kind) use ($palette): string { - $svg_open = ' $svg_open . 'viewBox="0 0 24 24" width="18" height="18">' - . '' + . '' . '', - // Latin obelus/dagger — the typographic convention for "died" in - // obituaries since the 18th century. Restrained, narrow. + // Latin obelus — the typographic mark for "died" in obituaries. 'DEAT' => $svg_open . 'viewBox="0 0 24 24" width="14" height="18">' - . '' - . '' + . '' + . '' . '', - // Two interlocking rings — universal heraldic mark of marriage. + // Two interlocking rings — universal mark of marriage. 'MARR' => $svg_open . 'viewBox="0 0 36 22" width="28" height="18">' - . '' - . '' + . '' + . '' . '', default => '', @@ -121,25 +129,34 @@ $event_icon = static function (string $kind) use ($palette): string { }; /** - * A single circular avatar — either the embedded image, or a coloured - * disc with the person's initials. Wraps in an linking to the - * individual's webtrees page when available. + * Map an event type to the colour used for that row's timeline dot + * (so the right rail reads as a colour-coded ribbon). */ +$event_color = static function (string $kind) use ($palette): string { + return match ($kind) { + 'BIRT' => $palette['birth'], + 'DEAT' => $palette['death'], + 'MARR' => $palette['marr'], + default => $palette['ink3'], + }; +}; + $avatar = static function (Individual|null $individual) use ($avatar_cids, $avatar_size, $palette): string { if (!$individual instanceof Individual) { return ''; } - $alt = e(strip_tags($individual->fullName())); - - $shadow = 'box-shadow:0 0 0 1px ' . $palette['rule'] . ',0 2px 6px rgba(58,40,32,0.18);'; + $alt = e(strip_tags($individual->fullName())); + $shadow = 'box-shadow:0 0 0 1px ' . $palette['border'] . ',0 1px 3px rgba(0,0,0,0.08);'; if (isset($avatar_cids[$individual->xref()])) { - $cid = $avatar_cids[$individual->xref()]; - $inner = '' . $alt . 'xref()]; + $inner = '' . $alt . ''; } else { + // Initial-disc fallback. Hue is hashed from the xref so the + // same person keeps the same colour across editions. $hue = hexdec(substr(md5($individual->xref()), 0, 2)) * 360 / 255; $first = strip_tags($individual->getAllNames()[0]['givn'] ?? $individual->xref()); $last = strip_tags($individual->getAllNames()[0]['surn'] ?? ''); @@ -147,9 +164,9 @@ $avatar = static function (Individual|null $individual) use ($avatar_cids, $avat $inner = '' . $initials . ''; + . 'border-radius:50%;background:hsl(' . (int) $hue . ',32%,60%);color:#fff;' + . "font:600 19px/{$avatar_size}px " . $font_stack . ';text-align:center;' + . 'letter-spacing:0.3px;' . $shadow . '">' . $initials . ''; } return '' . $inner . ''; @@ -171,21 +188,16 @@ $record_avatars = static function (Fact $fact) use ($avatar): string { } } - // Two circles overlapping by a few px reads as "couple" without - // needing additional glue characters. return '' . '' - . '' - . (isset($parts[1]) - ? '' - : '') + . (isset($parts[1]) ? '' : '') . '
' . ($parts[0] ?? '') . '' . $parts[1] . '' . $parts[1] . '
'; } return ''; }; -// Locale-aware ordinal: English uses st/nd/rd/th; German just appends ".". +// Locale-aware ordinal: German uses "N.", English uses st/nd/rd/th. $ordinal = static function (int $n): string { if (str_starts_with(I18N::languageTag(), 'de')) { return $n . '.'; @@ -240,48 +252,6 @@ $anniversary_label = static function (int $age) use ($ordinal): string { : I18N::translate('Wedding anniversary'); }; -// Styles reused across the event lists. -$row_padding = '20px 0'; -$divider_style = 'border-bottom:1px solid ' . $palette['rule'] . ';'; - -$section_title_style = 'margin:48px 0 8px;' - . 'font-family:' . $serif_display . ';' - . 'font-weight:500;font-size:28px;line-height:1.1;' - . 'color:' . $palette['ink'] . ';' - . 'letter-spacing:0.005em;'; - -$section_kicker_style = 'margin:0 0 22px;' - . 'font-family:' . $serif_display . ';' - . 'font-style:italic;font-size:15px;' - . 'color:' . $palette['mute'] . ';' - . 'letter-spacing:0.02em;'; - -$timeline_cell_style = 'width:170px;vertical-align:top;' - . 'padding:24px 0 24px 28px;' - . 'border-left:1px solid ' . $palette['rule'] . ';' - . 'font-family:' . $serif_body . ';' - . 'color:' . $palette['mute'] . ';' - . 'font-size:12px;letter-spacing:0.12em;text-transform:uppercase;' - . 'white-space:nowrap;'; - -$timeline_dot_style = 'display:inline-block;' - . 'width:11px;height:11px;background:' . $palette['gold'] . ';' - . 'border-radius:50%;' - . 'margin-left:-34px;margin-right:14px;vertical-align:middle;'; - -$content_cell_style = 'vertical-align:middle;' - . 'padding:24px 24px 24px 18px;' - . 'font-family:' . $serif_body . ';font-size:16px;line-height:1.4;' - . 'color:' . $palette['ink'] . ';'; - -$avatar_cell_style = 'width:64px;vertical-align:middle;padding:18px 0;'; - -// Renders one event row in the consistent 3-column layout shared by all -// sections (avatar | content | timeline + date). -/** - * Locale-aware long-form date for the masthead. German edition reads - * "15. Mai 2026" rather than the English "May 15, 2026". - */ $masthead_date = static function (int $timestamp): string { if (str_starts_with(I18N::languageTag(), 'de')) { $months = [ @@ -297,150 +267,184 @@ $masthead_date = static function (int $timestamp): string { return date('F j, Y', $timestamp); }; -$event_row = static function (Fact $fact, string $body_html) +// ─── Row styles ───────────────────────────────────────────────────────── + +$section_title_style = 'margin:0 0 4px;' + . 'font-family:' . $font_stack . ';' + . 'font-weight:400;font-size:24px;line-height:1.2;' + . 'color:' . $palette['ink'] . ';' + . 'letter-spacing:-0.005em;'; + +$section_kicker_style = 'margin:0 0 18px;' + . 'font-family:' . $font_stack . ';' + . 'font-style:italic;font-weight:300;font-size:14px;' + . 'color:' . $palette['ink3'] . ';'; + +$divider_style = 'border-bottom:1px solid ' . $palette['border'] . ';'; + +$avatar_cell_style = 'width:72px;vertical-align:middle;padding:18px 0 18px 18px;'; + +$content_cell_style = 'vertical-align:middle;' + . 'padding:18px 18px 18px 16px;' + . 'font-family:' . $font_stack . ';font-size:15px;line-height:1.4;' + . 'font-weight:300;color:' . $palette['ink'] . ';'; + +$timeline_cell_style = 'width:170px;vertical-align:middle;' + . 'padding:18px 18px 18px 26px;' + . 'border-left:1px solid ' . $palette['border'] . ';' + . 'font-family:' . $font_stack . ';' + . 'font-weight:500;font-size:11px;letter-spacing:0.12em;text-transform:uppercase;' + . 'color:' . $palette['ink3'] . ';white-space:nowrap;'; + +// Renders one event row in the consistent 3-column layout shared by +// every section (avatar | content | date on the timeline rail). +$event_row = static function (Fact $fact, string $body_html, string $dot_color) use ( $record_avatars, $event_date_display, $avatar_cell_style, $content_cell_style, $timeline_cell_style, - $timeline_dot_style, $divider_style, ): string { return '' . '' . $record_avatars($fact) . '' . '' . $body_html . '' . '' - . '' + . '' . e($event_date_display($fact)) . '' . ''; }; +// Wraps a list of rows in the "card" surface — rounded background +// patch matching .card on the website. +$card_open = ''; +$card_close = '
'; + ?> -<?= e(I18N::translate('Family newsletter — %s', date('F j, Y', $generated_at))) ?> +<?= e(I18N::translate('Family newsletter — %s', $masthead_date($generated_at))) ?> - + + style="width:100%;background:;"> -
+ + style="width:100%;max-width:720px;background:;font-family:;"> - - - - - - isEmpty()) : ?> - isEmpty()) : ?> - isEmpty()) : ?> - -
-
+
+
-

+

title()) ?>

-
+
+ · +
-
-
-
-
+

- + ' - . '' . $record_label($fact) . '' - . ' — ' - . e($birthday_label($age)) . '' + $body = '' . $event_icon('BIRT') . '' + . '' + . '' . $record_label($fact) . '' + . ' — ' . e($birthday_label($age)) . '' . ''; - echo $event_row($fact, $body); + echo $event_row($fact, $body, $palette['birth']); ?> -
+
+

- + ' - . '' . $record_label($fact) . '' - . ' — ' - . e($anniversary_label($age)) . '' + $body = '' . $event_icon('MARR') . '' + . '' + . '' . $record_label($fact) . '' + . ' — ' . e($anniversary_label($age)) . '' . ''; - echo $event_row($fact, $body); + echo $event_row($fact, $body, $palette['marr']); ?> -
+
+

- + ' - . '' . $record_label($fact) . '' - . ' — ' - . e($fact->label()) . '' + $body = '' . $event_icon($kind) . '' + . '' + . '' . $record_label($fact) . '' + . ' — ' . e($fact->label()) . '' . ''; - echo $event_row($fact, $body); + echo $event_row($fact, $body, $event_color($kind)); ?> -
+
-
-

+

+
+

title())) ?>
' . e(I18N::translate('My account')) . '', + '' . e(I18N::translate('My account')) . '', ) ?>