diff --git a/assets/styles/app.css b/assets/styles/app.css index 268b65c..a2de006 100644 --- a/assets/styles/app.css +++ b/assets/styles/app.css @@ -139,9 +139,45 @@ svg.icon { border-radius: 0; /* Sharp edges */ } +/* Landing (home): clear hierarchy — section label / title / excerpt / spacing */ +.home-body { + display: flex; + flex-direction: column; + gap: 3.5rem; +} + +/* List pages: space header / form from content (same intent as .home-body gap) */ +.search-page { + display: flex; + flex-direction: column; + gap: 2.5rem; +} + +.category-page__header-card { + margin-bottom: 2.5rem; +} + +/* Author profile: space articles block below header+divider (header is multiple nodes; do not use flex+gap on .author-profile) */ +.author-profile > .author-profile__divider { + margin: 2.5rem 0; + border: 0; + border-top: 1px solid var(--color-border); +} + .featured-cat { - border-bottom: 2px solid var(--color-border); - padding-left: 10px; + border-bottom: 1px solid var(--color-border); + padding: 0 0 0.75rem 10px; + margin-bottom: 1.25rem; +} + +.featured-cat small { + display: block; + font-size: 0.72rem; + font-weight: 600; + letter-spacing: 0.06em; + text-transform: uppercase; + color: color-mix(in srgb, var(--color-text-mid) 55%, var(--color-bg) 45%); + line-height: 1.35; } .featured-list { @@ -149,18 +185,50 @@ svg.icon { flex-direction: row; flex-wrap: nowrap; align-items: flex-start; + gap: 1.25rem; } - .featured-list > * { - box-sizing: border-box; /* so padding/border don't break the layout */ - margin-bottom: 10px; - padding: 10px; + box-sizing: border-box; + margin: 0; + padding: 0; + min-width: 0; +} + +/* Uniform text padding for lead + side cards; image is full-bleed above. */ +.featured-list .card-body { + box-sizing: border-box; + padding: 1rem 1.125rem 1.2rem; +} + +/* Lead card only: 16:9 cover frame (Molecules/FeaturedList — side cards have no .card-header). */ +.featured-list .card > a > .card-header { + margin: 0; + width: 100%; + aspect-ratio: 16 / 9; + overflow: hidden; + background-color: var(--color-bg-light); +} + +.featured-list .card > a > .card-header img { + width: 100%; + height: 100%; + object-fit: cover; + object-position: center; + display: block; +} + +/* Site logo fallback (small square asset in a wide frame) */ +.featured-list .card > a > .card-header img[src*="favicon-96x96"] { + object-fit: contain; + padding: 2.25rem; + box-sizing: border-box; } @media (max-width: 1024px) { .featured-list { flex-direction: column !important; + gap: 1.5rem; } .featured-list > div:first-child, @@ -168,19 +236,6 @@ svg.icon { flex: 1 1 auto; width: 100%; } - - .featured-list .card-header { - margin-top: 20px; - } - - .featured-list .card { - border-bottom: 1px solid var(--color-border) !important; - } - - .featured-list > * { - margin-bottom: 10px; - padding: 0; - } } div:nth-child(odd) .featured-list { flex-direction: row-reverse; @@ -197,25 +252,81 @@ div:nth-child(odd) .featured-list { min-width: 0; } +/* Each column: vertical rhythm between hero / stacked cards (replaces ad-hoc card margins). */ +.featured-list > div { + display: flex; + flex-direction: column; + gap: 0.85rem; +} + +/* List card titles + excerpts: home (featured), category, search, author */ +.featured-list h2.card-title, +.article-list h2.card-title { + font-family: var(--heading-font), serif; + font-size: 1.95rem; + font-weight: 700; + line-height: 1.18; + color: var(--color-primary); + margin: 0.15rem 0 0.55rem; +} + +/* Home featured grid only: two-line title + two-line deck for even rhythm. */ .featured-list h2.card-title { - font-size: 1.5rem; + display: -webkit-box; + -webkit-box-orient: vertical; + -webkit-line-clamp: 2; + line-clamp: 2; + overflow: hidden; + min-height: 2.36em; + margin-top: 0; + margin-bottom: 0.4rem; } -.featured-list p.lede { - font-size: 1.4rem; +.featured-list p.lede, +.article-list p.lede { + font-family: var(--main-body-font), serif; + font-size: 1.1rem; + font-weight: 400; + line-height: 1.55; + color: var(--color-text-mid); + margin-top: 0.15rem; } -.featured-list .card { - margin-bottom: 10px; +.featured-list p.lede.truncate { + -webkit-line-clamp: 2; + line-clamp: 2; } -.featured-list .card:not(:last-child) { - border-bottom: 1px solid var(--color-border); +.featured-list__meta { + font-family: var(--font-family), sans-serif; + font-size: 0.78rem; + font-weight: 400; + line-height: 1.35; + color: color-mix(in srgb, var(--color-text-mid) 48%, var(--color-bg) 52%); + margin: 0.15rem 0 0.45rem; } -.featured-list .card-header img { - max-height: 500px; - aspect-ratio: 1; +.featured-list__meta time { + color: inherit; +} + +/* Whole-card link is `color: inherit`; keep excerpt + date subdued on hover. */ +.featured-list .card a:hover p.lede, +.article-list .card a:hover p.lede { + color: color-mix(in srgb, var(--color-text-mid) 88%, var(--color-text) 12%); + text-decoration: none; +} + +.featured-list .card a:hover .featured-list__meta { + color: color-mix(in srgb, var(--color-text-mid) 58%, var(--color-text) 42%); +} + +.featured-list .card { + margin: 0; + border: 1px solid var(--color-border); + box-sizing: border-box; + min-width: 0; + background: var(--color-bg); } .article-list .metadata { @@ -225,11 +336,48 @@ div:nth-child(odd) .featured-list { align-items: center; gap: 0.75rem; min-width: 0; + font-family: var(--font-family), sans-serif; + font-size: 0.78rem; + font-weight: 400; + line-height: 1.35; + color: color-mix(in srgb, var(--color-text-mid) 48%, var(--color-bg) 52%); } .article-list .metadata p { margin: 0; min-width: 0; + color: inherit; +} + +.article-list .metadata a { + color: color-mix(in srgb, var(--color-text-mid) 72%, var(--color-text) 28%); + text-decoration: none; +} + +.article-list .metadata a:hover, +.article-list .metadata a:focus-visible { + color: var(--color-link-hover); + text-decoration: underline; + text-underline-offset: 2px; +} + +/* List cards: same site-logo treatment when the hero is the default mark */ +.article-list .card-header img[src*="favicon-96x96"] { + object-fit: contain; + padding: 1.25rem; + box-sizing: border-box; + background: var(--color-bg-light); +} + +/* Optional category label above cover (see Molecules/Card) */ +.article-list .card-header small { + display: block; + font-size: 0.72rem; + font-weight: 600; + letter-spacing: 0.05em; + text-transform: uppercase; + color: color-mix(in srgb, var(--color-text-mid) 55%, var(--color-bg) 45%); + margin-bottom: 0.35rem; } .truncate { @@ -294,21 +442,79 @@ div:nth-child(odd) .featured-list { display: flex; flex-wrap: wrap; justify-content: center; - gap: 20px; - padding: 0; + align-items: center; + gap: 0.4rem 0.6rem; + padding: 0.35rem 0.5rem 0.5rem; + margin: 0; } .header__categories li { list-style: none; } -.header__categories li a:hover { +/* Top category row: current section + clear hover affordance (passive “clean” list → scannable) */ +.header__cat-link { + display: inline-flex; + align-items: center; + justify-content: center; + box-sizing: border-box; + padding: 0.4rem 0.75rem; + font-family: var(--font-family), sans-serif; + font-size: 0.82rem; + font-weight: 600; + letter-spacing: 0.04em; + text-transform: uppercase; text-decoration: none; - font-weight: bold; + color: var(--color-text-mid); + background: transparent; + border: 1px solid transparent; + border-radius: 5px; + transition: + color 0.16s ease, + background-color 0.16s ease, + border-color 0.16s ease, + box-shadow 0.16s ease; } -.header__categories a.active { - font-weight: bold; +.header__cat-link:hover { + text-decoration: none; + color: var(--color-primary); + background: color-mix(in srgb, var(--color-primary) 8%, var(--color-bg) 92%); + box-shadow: 0 2px 0 0 var(--color-secondary); +} + +.header__cat-link:focus-visible { + text-decoration: none; + outline: 2px solid var(--color-focus-ring); + outline-offset: 2px; +} + +.header__cat-link--active { + color: var(--color-primary); + font-weight: 700; + background: color-mix(in srgb, var(--color-primary) 14%, var(--color-bg) 86%); + box-shadow: inset 0 -2px 0 0 var(--color-primary); +} + +/* Strong CTA: “Latest Articles” (outline + fill when you’re on that list) */ +.header__cat-link--cta { + border-color: color-mix(in srgb, var(--color-secondary) 55%, var(--color-border) 45%); + color: var(--color-secondary); + background: color-mix(in srgb, var(--color-secondary) 7%, var(--color-bg) 93%); +} + +.header__cat-link--cta:hover { + color: var(--color-link-hover); + background: color-mix(in srgb, var(--color-secondary) 14%, var(--color-bg) 86%); + box-shadow: 0 2px 0 0 var(--color-secondary); +} + +.header__cat-link--cta.header__cat-link--active { + color: var(--color-text-contrast); + background: var(--color-primary); + border-color: var(--color-primary); + font-weight: 700; + box-shadow: none; } .header__logo h1 { @@ -541,6 +747,11 @@ footer a { .author-profile__title { margin-top: 0.25em; + font-family: var(--heading-font), serif; + font-size: clamp(1.65rem, 3.2vw, 2.35rem); + font-weight: 700; + line-height: 1.12; + color: var(--color-primary); } .author-profile__header-meta { diff --git a/assets/styles/article.css b/assets/styles/article.css index 3c97e36..4f83883 100644 --- a/assets/styles/article.css +++ b/assets/styles/article.css @@ -50,6 +50,26 @@ flex: 1 1 12rem; min-width: 0; margin: 0; + font-family: var(--heading-font), serif; + font-weight: 700; + line-height: 1.12; + color: var(--color-primary); +} + +/* Article + category page headers: global h1 is 300 weight; titles should read as the clear focal point. */ +.card-header--article h1.card-title { + font-size: clamp(1.85rem, 2.8vw, 2.75rem); +} + +/* Hero summary: same “excerpt” level as list cards, not .lede’s 1.6rem body scale */ +.card > .card-body > .lede { + font-family: var(--main-body-font), serif; + font-size: 1.1rem; + line-height: 1.55; + font-weight: 400; + color: var(--color-text-mid); + margin: 0 0 1.25rem; + max-width: none; } /* Sibling .category-body would paint over the ⋯ popover; lift the title card above the list. */ @@ -76,7 +96,10 @@ margin: 2rem 0; padding-top: 0.5rem; border-top: 1px solid var(--color-border); - font-size: 1rem; + font-size: 0.88rem; + font-weight: 400; + color: color-mix(in srgb, var(--color-text-mid) 58%, var(--color-bg) 42%); + font-family: var(--font-family), sans-serif; } .byline__author { diff --git a/assets/styles/card.css b/assets/styles/card.css index 1c8cbcd..7c62526 100644 --- a/assets/styles/card.css +++ b/assets/styles/card.css @@ -36,7 +36,7 @@ h2.card-title { } .article-list .card { - margin-bottom: 1rem; + margin-bottom: 1.75rem; min-width: 0; /* column flex: do not let cover images set unshrinkable row width */ } diff --git a/assets/styles/event.css b/assets/styles/event.css index c859c8d..3af21d6 100644 --- a/assets/styles/event.css +++ b/assets/styles/event.css @@ -94,8 +94,11 @@ flex-wrap: wrap; gap: 0.5rem; width: 100%; - color: var(--color-text-mid); - font-size: 0.95rem; + font-family: var(--font-family), sans-serif; + font-size: 0.78rem; + font-weight: 400; + line-height: 1.35; + color: color-mix(in srgb, var(--color-text-mid) 50%, var(--color-bg) 50%); } .event-page a:focus-visible { diff --git a/assets/styles/layout.css b/assets/styles/layout.css index 0c57460..d8c3a16 100644 --- a/assets/styles/layout.css +++ b/assets/styles/layout.css @@ -144,7 +144,11 @@ width: 100%; text-align: left; padding: 0.45rem 0.75rem; - font: inherit; + font-family: var(--font-family), sans-serif; + font-size: 0.9rem; + font-weight: 400; + line-height: 1.3; + text-transform: none; color: var(--color-text, inherit); text-decoration: none; background: none; @@ -258,7 +262,13 @@ a.nostr-share-menu__action { .header__categories ul { flex-direction: column; - gap: 10px; + gap: 0.35rem; + align-items: stretch; + } + + .header__cat-link { + width: 100%; + min-height: 2.6rem; } /* Log in / account block below category links in the hamburger */ @@ -641,6 +651,9 @@ footer .footer-links { max-width: 48rem; margin: 0 auto; padding: 0 0.5rem 2rem; + display: flex; + flex-direction: column; + gap: 2.5rem; } .featured-authors-grid { @@ -709,7 +722,7 @@ footer .footer-links { } .featured-authors__intro { - margin-bottom: 2rem; + margin-bottom: 0; overflow: visible; /* do not clip heading ascenders */ } @@ -718,7 +731,7 @@ footer .footer-links { margin: 0 0 0.5rem; font-size: clamp(1.35rem, 2.6vw, 2.05rem); line-height: 1.28; - font-weight: 500; + font-weight: 700; font-family: var(--heading-font), serif; color: var(--color-primary); padding: 0.2em 0 0.05em; @@ -807,17 +820,14 @@ footer .footer-links { text-align: center; } -/* Narrow: smaller page title + intro; flex gap avoids margin collapse with first author card. */ +/* Narrow: smaller page title + intro; flex gap avoids margin collapse with first author block. */ @media (max-width: 1024px) { .featured-authors { - display: flex; - flex-direction: column; align-items: stretch; gap: 1.75rem; } .featured-authors__intro { - margin-bottom: 0; /* Contain the intro
margin so it doesn’t collapse with the first author block */ display: flow-root; } diff --git a/src/Twig/ArticleCardCoverExtension.php b/src/Twig/ArticleCardCoverExtension.php new file mode 100644 index 0000000..e6e7d5f --- /dev/null +++ b/src/Twig/ArticleCardCoverExtension.php @@ -0,0 +1,96 @@ + lowercase 64-hex pubkey → resolved cover URL (author picture or site default) + */ + private array $authorCoverMemo = []; + + public function __construct( + private readonly CacheService $cacheService, + private readonly NostrPathHelper $nostrPathHelper, + private readonly Packages $packages, + ) { + } + + public function getFunctions(): array + { + return [ + new TwigFunction('article_card_cover', $this->articleCardCover(...)), + ]; + } + + /** + * @param string|null $articleImage Cover URL stored on the article, if any + * @param string|null $pubkeyHex 64-char hex (lowercase) Nostr public key, if any + */ + public function articleCardCover(?string $articleImage, ?string $pubkeyHex): string + { + if ($articleImage !== null && trim($articleImage) !== '') { + return trim($articleImage); + } + + $pubkeyHex = $pubkeyHex !== null ? strtolower(trim($pubkeyHex)) : ''; + if (64 !== strlen($pubkeyHex) || !ctype_xdigit($pubkeyHex)) { + return $this->defaultSiteImageUrl(); + } + + if (\array_key_exists($pubkeyHex, $this->authorCoverMemo)) { + return $this->authorCoverMemo[$pubkeyHex]; + } + + try { + $npub = $this->nostrPathHelper->npubFromPubkeyHex($pubkeyHex); + if ($npub === '') { + $url = $this->defaultSiteImageUrl(); + $this->authorCoverMemo[$pubkeyHex] = $url; + + return $url; + } + + $meta = $this->cacheService->getMetadata($npub); + $pic = isset($meta->picture) ? trim((string) $meta->picture) : ''; + if ($pic !== '') { + $this->authorCoverMemo[$pubkeyHex] = $pic; + + return $pic; + } + } catch (Throwable) { + $out = $this->defaultSiteImageUrl(); + $this->authorCoverMemo[$pubkeyHex] = $out; + + return $out; + } + + $out = $this->defaultSiteImageUrl(); + $this->authorCoverMemo[$pubkeyHex] = $out; + + return $out; + } + + private function defaultSiteImageUrl(): string + { + return $this->packages->getUrl(self::DEFAULT_PACKAGE_IMAGE); + } +} diff --git a/templates/components/Header.html.twig b/templates/components/Header.html.twig index c978cd2..32a478e 100644 --- a/templates/components/Header.html.twig +++ b/templates/components/Header.html.twig @@ -18,8 +18,13 @@