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") %}
-
+ A donation is the fastest way to make a difference today.
+ Prefer a different way to pitch in? The options below cover
+ membership, working groups, and merch too.
+