diff --git a/content/foundation/members/members.toml b/content/foundation/members/members.toml index 30a1b39eb9..bf56a4a2ef 100644 --- a/content/foundation/members/members.toml +++ b/content/foundation/members/members.toml @@ -22,6 +22,7 @@ logo = "connect2x.png" name = "DINUM" website = "https://www.numerique.gouv.fr/" logo = "dinum.svg" +white_bg = true [[silver]] name = "Discourse" @@ -112,6 +113,7 @@ logo = "cinny.svg" name = "Community Moderation Effort" website = "" logo = "community-moderation-effort.png" +white_bg = true [[ecosystem]] name = "Conduit" @@ -127,6 +129,7 @@ logo = "continuwuity.svg" name = "Draupnir" website = "https://github.com/the-draupnir-project/Draupnir" logo = "draupnir.png" +white_bg = true [[ecosystem]] name = "Elm SDK" @@ -137,6 +140,7 @@ logo = "elm-sdk.svg" name = "FluffyChat" website = "https://fluffychat.im/" logo = "fluffychat.svg" +white_bg = true [[ecosystem]] name = "Fractal" @@ -167,6 +171,7 @@ logo = "polychat.svg" name = "Rory&::LibMatrix" website = "https://cgit.rory.gay/matrix" logo = "rory-libmatrix.png" +white_bg = true [[ecosystem]] name = "Thunderbird" diff --git a/sass/_base.scss b/sass/_base.scss index 95e1354d3d..fa5ac01d43 100644 --- a/sass/_base.scss +++ b/sass/_base.scss @@ -18,6 +18,31 @@ --borders-color: #ddd; --notice-box-color: #ffcb006b; + + // Tokens for the support/membership card redesign (_support.scss). + // Kept distinct from the older --borders-color/--color-text-light pair + // above rather than merged into them, since those are already used + // with their own (slightly different) shades elsewhere. + --color-border: #d2d2d2; + --color-text-muted: #555; + --color-bg-tint: #fafafa; + --card-radius: 16px; + --card-radius-sm: 8px; + + // The top 3 "become a member" tiers are literally named after materials + // (Platinum/Gold/Silver) - used as a top-edge accent on .tier-card and a + // frame/flag around the matching tier of supporter logos (_support.scss). + // Platinum is deliberately lighter/cooler and Silver deliberately + // darker/warmer so the two don't read as the same grey. These plain + // colours are for the (thin, non-text) frame border only; the flag is a + // solid fill with white text, which needs its own darker "-badge" pair + // to clear AA (the plain colours are too light/desaturated for that). + --color-tier-platinum: #b8c4cc; + --color-tier-platinum-badge: #5b6b74; + --color-tier-gold: #c9a227; + --color-tier-gold-badge: #8a6d1c; + --color-tier-silver: #7c7f83; + --color-tier-silver-badge: #5c5f63; } * { diff --git a/sass/_breadcrumb.scss b/sass/_breadcrumb.scss new file mode 100644 index 0000000000..5dcfc2f0e6 --- /dev/null +++ b/sass/_breadcrumb.scss @@ -0,0 +1,81 @@ +// Breadcrumb navigation +// Used via macros/breadcrumb.html. Two contexts: +// 1. Inside .page-header (dark background) - text is white/muted-white +// 2. Inside .detail-page (light background) - text is muted grey + +.breadcrumb { + ol { + display: flex; + flex-wrap: wrap; + align-items: center; + list-style: none; + margin: 0; + padding: 0; + gap: 0.25rem; + font-size: 0.85rem; + } + + li { + display: flex; + align-items: center; + + // Chevron separator before every item except the first + & + li::before { + content: "/"; + margin-right: 0.25rem; + opacity: 0.45; + } + } + + a, + span { + text-decoration: none; + line-height: 1; + } + + // Dark context (inside .page-header) + .page-header & { + margin-bottom: 0.75rem; + + a { + color: rgb(255 255 255 / 70%); + + &:hover { + color: #fff; + } + } + + // 45% white only clears ~4.4:1 against the black hero background, + // just under AA's 4.5:1 for text; 60% keeps it safely above. + span { + color: rgb(255 255 255 / 60%); + } + + li + li::before { + color: rgb(255 255 255 / 45%); + } + } + + // Light context (inside .detail-page) + .detail-page & { + margin-bottom: 1.25rem; + + a { + color: var(--color-text-muted); + + &:hover { + color: var(--color-text); + } + } + + // The current-page label is text, not decoration, so it still needs + // to clear WCAG AA (4.5:1); #aaa on white only manages ~2.3:1. + span { + color: var(--color-text-light); + } + + li + li::before { + color: #ccc; + } + } +} diff --git a/sass/_support.scss b/sass/_support.scss index 5958a4f551..cd85e264bf 100644 --- a/sass/_support.scss +++ b/sass/_support.scss @@ -1,37 +1,10 @@ -.support.main { - - &>div { - - padding-block: 3rem; - - @media (max-width: 767px) { - padding-block: 2rem; - } - - &:nth-child(even) { - background-color: #eee; - } - - .content { - display: grid; - gap: 1rem; - - h2 { - margin-block: 0; - } - - p:last-child { - margin-block-end: 0; - } - - .call-to-action { - justify-self: left; - } - } - - } - -} +// /support/ now reuses the shared .step-flow layout (_try-matrix.scss) +// instead of its own grid. The rest of this file (.membership tiers, +// #supporters wall) belongs to /foundation/members/ (membership.html). +// +// Breadcrumb nav (macros/breadcrumb.html) is styled by the shared +// _breadcrumb.scss partial - see there for the .page-header (dark) context +// both pages use. .membership { @@ -83,79 +56,264 @@ display: grid; grid-template-columns: repeat(4, 15rem); gap: 1rem; - overflow-y: auto; + // These 4 columns are wider than the viewport on most screens, so + // this scrolls sideways (the "scroll to most-popular" script in + // membership.html relies on scrollLeft); it was previously + // overflow-y, which didn't let that horizontal scroll happen at all. + overflow-x: auto; + scroll-snap-type: x proximity; padding-block-start: 1.5em; margin-block-end: 2em; .tier-card { - border: 1px solid var(--borders-color); - border-radius: 16px; + border: 1px solid var(--color-border); + border-top-width: 3px; + border-radius: var(--card-radius); background-color: #fff; + scroll-snap-align: start; display: flex; flex-direction: column; - padding: 1em; + gap: 0.6rem; + padding: 1.1em; + // The body-wide line-height (1.75) is meant for prose, not a + // dense list of short feature labels in a 15rem-wide card - + // it made wrapped lines look loosely, unevenly spaced. + line-height: 1.4; + + // These tiers are literally named after materials - give + // Silver/Gold/Platinum a top-edge accent in their namesake + // colour (--color-tier-*, _base.scss). Individual isn't a + // material name, so it keeps the plain border. + &--silver { + border-top-color: var(--color-tier-silver); + } + + &--gold { + border-top-color: var(--color-tier-gold); + } + + &--platinum { + border-top-color: var(--color-tier-platinum); + } + + // The individual tier is a different kind of thing than the 3 + // organisation tiers next to it, not just a cheaper option - + // give that group boundary a bit of visual room and a divider, + // centred in the combined grid-gap + margin space between them. + &--group-start { + position: relative; + margin-inline-start: 0.5rem; + + &::before { + content: ""; + position: absolute; + top: 0; + bottom: 0; + left: -0.75rem; + width: 1px; + background-color: var(--color-border); + } + } + + &-eyebrow { + margin: 0; + text-align: center; + font-size: 0.7rem; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.04em; + color: var(--color-text-muted); + } h3 { - margin-inline: auto; - margin-block-start: .5rem; + margin: 0; + text-align: center; } .pricing { + align-self: center; background-color: #e9e9e9; + border: 1px solid var(--color-border); border-radius: 9999px; - margin-inline: auto; - padding-inline: .8em; + padding: 0.15rem 0.8em; + font-weight: 700; } ul { - margin-block-start: 1rem; - padding-inline-start: 1em; + margin: 0; + padding-inline-start: 1.35em; li { list-style-image: url('/assets/tick_supported.svg'); - margin-block: .3em; + font-size: 0.9rem; + + & + li { + margin-block-start: 0.4em; + } } } } } } -#platinum-supporters .supporters-card { - --cards-per-row: 4; -} - -#gold-supporters .supporters-card { - --cards-per-row: 4; -} - -#silver-supporters .supporters-card { - --cards-per-row: 5; +// Fixed logo box (_support.scss below) + object-fit: contain, instead of +// deriving a width from page-max-width/cards-gap/tier count - simpler to +// read, and doesn't need re-deriving if those values ever change. See +// https://ishadeed.com/article/aligning-logos-css/ for the technique. +.supporters-card { + --supporters-logo-size: 145px; } -#associate-supporters .supporters-card { - --cards-per-row: 5; +// Membership contracts specify Platinum logos must render larger than +// Gold, which must render larger than Silver - a graduated step, not just +// a flat "top 3 tiers are bigger" split. +#platinum-supporters .supporters-card { + --supporters-logo-size: 190px; } -#ecosystem-supporters .supporters-card { - --cards-per-row: 5; +#gold-supporters .supporters-card { + --supporters-logo-size: 165px; } #supporters { display: flex; flex-direction: column; - max-width: var(--page-max-width); - margin-inline: auto; margin-block-end: 5rem; - --supporters-padding-inline: 1rem; - padding-inline: var(--supporters-padding-inline); + + // Full-bleed bands (same idea as .step-background in _try-matrix.scss) + // so each tier reads as its own section rather than all ~40 Silver/ + // Ecosystem/Associate logos blurring into one continuous field. + .supporters-band { + padding-block: 2.5rem; + padding-inline: 1rem; + + @media (max-width: 767px) { + padding-block: 1.5rem; + } + + &:nth-of-type(even) { + background-color: #f0f0f0; + } + } + + // Platinum/Gold/Silver side by side instead of stacked. Silver has far + // more members than Platinum/Gold (double digits vs. a couple), so it + // gets more room to flow into (enough for several logos per row instead + // of collapsing to a cramped single column). All 5 tiers sit in one + // flex-wrap row, in DOM order - true masonry (CSS grid-template-rows: + // masonry) is Firefox-only today, and CSS multi-column packs tighter but + // reorders content column-by-column instead of preserving DOM order, so + // neither is usable as the baseline. This plain flex-wrap row is what + // renders with no JS (and what a screen reader/no-JS browser sees): + // Platinum, Gold, Silver, Ecosystem, Associate always in that order, at + // every width - the only downside is that flex rows don't backfill gaps + // the way real masonry would, so a short tile next to a tall one can + // leave whitespace beside it. supporters-masonry.js progressively + // enhances this into hand-packed columns (see .js-masonry below) once + // it's had a chance to measure real rendered heights; anyone without JS, + // or before it's finished, still gets this fully working layout. + .supporters-tier-flow { + display: flex; + flex-wrap: wrap; + align-items: flex-start; + gap: 1.5rem; + max-width: var(--page-with-toc-max-width); + margin-inline: auto; + + .supporters-section { + max-width: none; + margin-inline: 0; + } + } + + // Rough size classes so tiles with more members get more room to + // spread their logos across, without needing an exact width per tier. + // Only meaningful in the flex-wrap (no-JS) row above; .js-masonry + // overrides these back to "fill the column" once JS has taken over. + .supporters-section--sm { + flex: 1 1 16rem; + } + + .supporters-section--md { + flex: 1 1 22rem; + } + + .supporters-section--lg { + flex: 3 1 32rem; + } + + // supporters-masonry.js's progressive enhancement: it measures each + // tile's natural height (from the flex-wrap row above) and greedily + // places each one, in DOM order, into whichever of these columns is + // currently shortest - the same packing algorithm libraries like + // Masonry.js use. Tiles no longer read strictly left-to-right/top-to- + // bottom, but every column still fills in DOM order, and the underlying + // DOM order (what assistive tech and view-source see) never changes - + // this only moves nodes visually. + .supporters-tier-flow.js-masonry { + flex-wrap: nowrap; + align-items: stretch; + + .supporters-tier-column { + display: flex; + flex-direction: column; + flex: 1 1 0; + min-width: 0; + gap: 1.5rem; + } + + // Every tile fills its column - the flex-basis weighting above only + // applies to the un-enhanced single-row layout. + .supporters-section { + flex: none; + width: 100%; + } + } .supporters-section { display: flex; flex-direction: column; + max-width: var(--page-max-width); + margin-inline: auto; + width: 100%; + + // Platinum/Gold/Silver get a frame in their namesake colour, sized + // to their own content (not the full band width) with a solid-fill + // flag sitting flush in the top-left corner - a bigger version of + // the same "this is a material tier" hint as .tier-card's top edge, + // without needing to colour every individual logo card inside it. + // Ecosystem/Associate get the same frame+flag shape for visual + // consistency, just in a neutral grey since they're not material + // names and shouldn't imply one. + &--platinum, + &--gold, + &--silver, + &--neutral { + position: relative; + border: 2px solid var(--tier-color); + border-radius: var(--card-radius); + padding: 2.75rem 1.5rem 1.5rem; + } - h2 { - text-align: center; + &--platinum { + --tier-color: var(--color-tier-platinum); + --tier-badge-color: var(--color-tier-platinum-badge); + } + + &--gold { + --tier-color: var(--color-tier-gold); + --tier-badge-color: var(--color-tier-gold-badge); + } + + &--silver { + --tier-color: var(--color-tier-silver); + --tier-badge-color: var(--color-tier-silver-badge); + } + + &--neutral { + --tier-color: var(--color-border); + --tier-badge-color: var(--color-text-muted); } .cards { @@ -166,14 +324,18 @@ gap: var(--cards-gap); justify-content: center; + // Matches .account-tier (_try-matrix.scss): a plain border, no + // background tint of its own - it just sits on whatever the + // band behind it already is. A separate card tint on top of the + // alternating band tint was two different greys stacked, which + // read as odd rather than as a clean "cell". .supporters-card { display: flex; flex-direction: column; justify-content: center; align-items: center; - --supporters-card-border-width: 1px; - border: var(--supporters-card-border-width) solid var(--borders-color); - border-radius: 16px; + border: 1px solid var(--color-border); + border-radius: var(--card-radius-sm); --supporters-card-hpadding: 8px; padding: var(--supporters-card-hpadding); // The whitespace of the text at the bottom visually adds to the padding so we account for this on the top a little. @@ -181,12 +343,78 @@ // This adds some space between the text and the image gap: var(--supporters-card-hpadding); + // These are links to the member's site, but had no colour + // override or hover/focus feedback, so they rendered as a + // plain underlined blue link name sitting under the logo. + color: inherit; + text-decoration: none; + outline: 2px solid transparent; + outline-offset: 2px; + transition: border-color 0.15s ease; + + &:hover { + border-color: #888; + } + + &:focus-visible { + outline-color: #000; + } + + span { + font-size: 0.85rem; + // The body-wide line-height (1.75) left wrapped 2-line + // company names looking loosely spaced in these small + // cards; overflow-wrap guards against long single-word + // names overflowing the fixed-width card. + line-height: 1.3; + text-align: center; + overflow-wrap: break-word; + } + + // Some source logo files have a lot of whitespace baked in + // around the mark, others are cropped tight - object-fit + // can't tell the difference, so those tight ones end up + // looking much bigger than the padded ones at the same box + // size. This inset padding doesn't fully equalise that (only + // re-cropping the source files would), but it shrinks every + // logo by the same amount, which shrinks the gap between + // them in absolute terms too. img { - aspect-ratio: 1/1; + box-sizing: border-box; + width: var(--supporters-logo-size, 145px); + height: var(--supporters-logo-size, 145px); + padding: 10px; object-fit: contain; - width: calc((var(--page-max-width) - 2* var(--supporters-padding-inline) - calc(var(--cards-per-row) - 1)* var(--cards-gap)) / var(--cards-per-row) - 2* var(--supporters-card-hpadding) - 2* var(--supporters-card-border-width)); + } + + // A few member logos ship with a baked-in white background + // (flagged via `white_bg = true` in members.toml), which + // shows as a visible white box against the card's tint. + // multiply drops the white and keeps the logo's own colours. + .white-bg-logo { + mix-blend-mode: multiply; } } } } + + // --tier-badge-color is set on .supporters-section--* above and + // inherited down to this span through the DOM, regardless of where this + // rule sits in the stylesheet. Sits flush in the frame's corner (not + // straddling the border like a badge-on-a-line) - its own top-left + // radius matches the frame's, so the two corners read as one shape. + .supporters-section-flag { + position: absolute; + // Offset by the frame's own border-width so the flag's outer corner + // aligns with the frame's outer corner exactly, instead of sitting + // one border-width inset from it. + top: -2px; + left: -2px; + padding: 0.4rem 1rem; + background-color: var(--tier-badge-color); + color: #fff; + font-size: 0.8rem; + font-weight: 700; + border-radius: var(--card-radius) 0 0.6rem 0; + } } diff --git a/sass/_try-matrix.scss b/sass/_try-matrix.scss index cc05e6741c..6e84f6497f 100644 --- a/sass/_try-matrix.scss +++ b/sass/_try-matrix.scss @@ -12,7 +12,11 @@ } } -.try_matrix { +// Generic "sequence of full-width steps" layout: alternating-background +// blocks, each a centred column of instructions + a CTA row. Originally +// built for try-matrix.html; reused by support.html and anywhere else that +// wants the same step-by-step feel instead of a card grid. +.step-flow { display: flex; flex-direction: column; diff --git a/sass/style.scss b/sass/style.scss index 93e0813b06..73435ad94d 100644 --- a/sass/style.scss +++ b/sass/style.scss @@ -1,6 +1,7 @@ @import '_normalize'; @import '_fonts'; @import '_base'; +@import '_breadcrumb'; @import '_tables'; @import '_footer'; @import '_header'; diff --git a/static/js/supporters-masonry.js b/static/js/supporters-masonry.js new file mode 100644 index 0000000000..61f19239ec --- /dev/null +++ b/static/js/supporters-masonry.js @@ -0,0 +1,94 @@ +// Progressive enhancement for the Platinum/Gold/Silver/Ecosystem/Associate +// tier tiles (#supporters .supporters-tier-flow in membership.html). +// +// Without this script, the tiles are a plain flex-wrap row (see +// .supporters-tier-flow in _support.scss) - it keeps them in DOM order +// (Platinum, Gold, Silver, Ecosystem, Associate) at every width, which is +// what a screen reader or a no-JS browser sees, but it can leave whitespace +// next to a short tile in the same row as a tall one, since flex rows don't +// backfill gaps from a later row. +// +// True masonry (grid-template-rows: masonry) is Firefox-only today, and CSS +// multi-column reorders content column-by-column instead of preserving +// reading order - neither is usable here. This does the packing by hand: +// measure each tile's natural height, then greedily place each one (in DOM +// order) into whichever column is currently shortest. That's the same +// algorithm libraries like Masonry.js use. Reading order is no longer +// strictly left-to-right top-to-bottom, but every tile still appears before +// the next columns starts filling behind it, and DOM order (and so the +// no-JS/reader order) is untouched - this only moves nodes visually. +(() => { + const flow = document.querySelector(".supporters-tier-flow"); + if (!flow) { + return; + } + + function columnCountFor(width) { + if (width < 700) { + return 1; + } + if (width < 1100) { + return 2; + } + return 3; + } + + // Captured once, before this script ever touches the DOM, so it always + // reflects the true Platinum/Gold/Silver/Ecosystem/Associate order - + // not document order, which stops matching that after the first layout + // pass moves tiles into columns (a resize re-running this would + // otherwise re-derive the processing order from wherever the previous + // pass happened to leave each tile, compounding drift on every resize). + const originalOrder = Array.from( + flow.querySelectorAll(".supporters-section") + ); + + function layout() { + // Undo any previous columns first, so heights are always measured + // from the flat flex-wrap state, not from inside a narrower column. + const items = originalOrder; + flow.classList.remove("js-masonry"); + for (const item of items) { + flow.appendChild(item); + } + for (const column of flow.querySelectorAll(".supporters-tier-column")) { + column.remove(); + } + + const columnCount = columnCountFor(flow.clientWidth); + if (columnCount <= 1) { + // The plain flex-wrap layout already reads fine as a single + // column; no need to introduce column wrappers for it. + return; + } + + const heights = items.map((item) => item.offsetHeight); + + const columns = Array.from({ length: columnCount }, () => { + const column = document.createElement("div"); + column.className = "supporters-tier-column"; + return column; + }); + const columnHeights = new Array(columnCount).fill(0); + + items.forEach((item, i) => { + const shortest = columnHeights.indexOf(Math.min(...columnHeights)); + columns[shortest].appendChild(item); + columnHeights[shortest] += heights[i]; + }); + + flow.classList.add("js-masonry"); + for (const column of columns) { + flow.appendChild(column); + } + } + + let resizeTimeout; + function scheduleLayout() { + clearTimeout(resizeTimeout); + resizeTimeout = setTimeout(layout, 150); + } + + document.addEventListener("DOMContentLoaded", layout); + window.addEventListener("resize", scheduleLayout); +})(); diff --git a/templates/macros/breadcrumb.html b/templates/macros/breadcrumb.html new file mode 100644 index 0000000000..506f8b8d79 --- /dev/null +++ b/templates/macros/breadcrumb.html @@ -0,0 +1,18 @@ +{% macro breadcrumb(l1_label, l1_path, l2_label="", l2_path="", l3_label="") %} + +{% endmacro %} diff --git a/templates/membership.html b/templates/membership.html index 361fd68114..22acc7630f 100644 --- a/templates/membership.html +++ b/templates/membership.html @@ -1,5 +1,6 @@ {% extends "page.html" %} {% import "macros/banner.html" as banner %} +{% import "macros/breadcrumb.html" as breadcrumb %} {% block head_extra %} + {% endblock head_extra %} {% block content %} {% set supporters = load_data(path="content/foundation/members/members.toml") %} -