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..894a6203af 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: #4596a3;
+ --color-tier-platinum-badge: #1c3c41;
+ --color-tier-gold: #d48f2f;
+ --color-tier-gold-badge: #4b300d;
+ --color-tier-silver: #969eaa;
+ --color-tier-silver-badge: #3c3f44;
}
* {
@@ -152,6 +177,10 @@ pre table {
@media (max-width: 767px) {
padding-inline: 2rem;
+ // The announcement banner sits directly above .page-header with no
+ // gap of its own, so on mobile (where the banner text wraps taller)
+ // the breadcrumb ends up crammed right underneath it.
+ padding-top: 1.5rem;
}
padding-bottom: calc(var(--navbar-height)/2);
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/_membership.scss b/sass/_membership.scss
new file mode 100644
index 0000000000..2fcd4333a1
--- /dev/null
+++ b/sass/_membership.scss
@@ -0,0 +1,249 @@
+.membership {
+ max-width: 65rem;
+ padding-inline: 1rem;
+ margin-inline: auto;
+
+ h3 {
+ font-size: 1.25rem;
+ }
+
+ .row {
+ display: flex;
+ flex-direction: row;
+ gap: 2rem;
+ margin-block: 2rem 3rem;
+
+ @media (max-width: 767px) {
+ flex-direction: column;
+ }
+ }
+
+ .representatives {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ flex: 1;
+
+ h3 {
+ text-align: center;
+ }
+
+ ul {
+ padding-inline: 0;
+ width: 100%;
+ }
+
+ li {
+ list-style: none;
+ text-align: center;
+ background-color: #fafafa;
+ border-radius: 8px;
+ margin-block: 0.5em;
+ padding: 0.5em 1rem;
+ }
+ }
+
+ .tiers-row-wrap {
+ position: relative;
+ }
+
+ .tiers-scroll-hint {
+ display: none;
+ position: absolute;
+ // Set by JS (membership.html) to the real vertical centre of the
+ // first card; this static fallback is only an approximation
+ // (roughly half the card row's own height down from its top) for
+ // when JS hasn't run yet.
+ top: var(--tiers-scroll-hint-top, calc(1.5em + 7.75rem));
+ transform: translateY(-50%);
+ z-index: 3;
+ width: 2.25rem;
+ height: 2.25rem;
+ border-radius: 50%;
+ background-color: rgba(0, 0, 0, 0.55);
+ align-items: center;
+ justify-content: center;
+ transition: opacity 0.2s ease;
+ text-decoration: none;
+
+ img {
+ width: 1rem;
+ height: 1rem;
+ // Base asset points down; point it sideways instead.
+ filter: invert(1);
+ }
+
+ // Real links to the first/last card (not JS-only), so the row is
+ // still reachable end-to-end without JS - see membership.html.
+ &--left {
+ left: 0.5rem;
+ // Sane default before JS runs / without JS: hidden, since a
+ // fresh page load starts scrolled to the left already.
+ opacity: 0;
+
+ img {
+ transform: rotate(90deg);
+ }
+ }
+
+ &--right {
+ right: 0.5rem;
+ // Sane default: visible, since there's normally more to the
+ // right to scroll to on a fresh load.
+ opacity: 1;
+
+ img {
+ transform: rotate(-90deg);
+ }
+ }
+
+ @media (max-width: 767px) {
+ display: flex;
+ }
+ }
+
+ .tiers-row-wrap.has-scroll-left .tiers-scroll-hint--left {
+ opacity: 1;
+ }
+
+ .tiers-row-wrap.no-scroll-right .tiers-scroll-hint--right {
+ opacity: 0;
+ }
+
+ .tiers-row {
+ display: grid;
+ grid-template-columns: repeat(4, 15rem);
+ gap: 1rem;
+ // 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(--color-border);
+ border-top-width: 3px;
+ border-radius: var(--card-radius);
+ background-color: #fff;
+ scroll-snap-align: start;
+
+ display: flex;
+ flex-direction: column;
+ 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: 0;
+ text-align: center;
+ }
+
+ .pricing {
+ align-self: center;
+ background-color: #e9e9e9;
+ border: 1px solid var(--color-border);
+ border-radius: 9999px;
+ padding: 0.15rem 0.8em;
+ font-weight: 700;
+ }
+
+ ul {
+ margin: 0;
+ padding-inline-start: 1.35em;
+
+ li {
+ list-style-image: url("/assets/tick_supported.svg");
+ font-size: 0.9rem;
+
+ & + li {
+ margin-block-start: 0.4em;
+ }
+ }
+ }
+ }
+
+ // Sit as extra grid items below the card row (grid-auto-flow keeps
+ // the default "sparse" packing, so a col-1 item and a col-2/5 item
+ // land on a new row rather than squeezing into row 1) - inside the
+ // same scrolling grid as the cards so they track the row's own
+ // horizontal scroll on mobile instead of drifting out of alignment.
+ .tiers-row-cta {
+ display: flex;
+
+ .call-to-action {
+ width: 100%;
+ text-align: center;
+ padding: 0.5em 1em;
+ // .call-to-action's own font-size (_base.scss) is sized for
+ // full-width page CTAs and is too large for this 15rem-wide
+ // column; match the tier-card's own list-item text size
+ // (ul li, above) instead of picking a new magic number.
+ font-size: 0.9rem;
+ }
+
+ &--individual {
+ grid-column: 1;
+ }
+
+ &--orgs {
+ grid-column: 2 / 5;
+ justify-content: center;
+
+ .call-to-action {
+ width: auto;
+ }
+ }
+ }
+ }
+}
diff --git a/sass/_support.scss b/sass/_support.scss
index 5958a4f551..0e31215f56 100644
--- a/sass/_support.scss
+++ b/sass/_support.scss
@@ -1,161 +1,187 @@
-.support.main {
+// 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;
+}
- &>div {
+// Platinum and Gold render at the same, larger size; only Silver (and
+// below) stay at the base size.
+#platinum-supporters .supporters-card,
+#gold-supporters .supporters-card {
+ --supporters-logo-size: 190px;
+}
+
+#supporters {
+ display: flex;
+ flex-direction: column;
+ margin-block-end: 5rem;
- padding-block: 3rem;
+ // 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: 2rem;
+ padding-block: 1.5rem;
}
- &:nth-child(even) {
- background-color: #eee;
+ &:nth-of-type(even) {
+ background-color: #f0f0f0;
}
+ }
- .content {
- display: grid;
- gap: 1rem;
-
- h2 {
- margin-block: 0;
- }
-
- p:last-child {
- margin-block-end: 0;
- }
-
- .call-to-action {
- justify-self: left;
- }
+ // 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. members-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;
}
-
}
-}
-
-.membership {
-
- max-width: 65rem;
- padding-inline: 1rem;
- margin-inline: auto;
+ // 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;
+ }
- h3 {
- font-size: 1.25rem;
+ .supporters-section--md {
+ flex: 1 1 22rem;
}
- .row {
- display: flex;
- flex-direction: row;
- gap: 2rem;
- margin-block: 2rem 3rem;
+ .supporters-section--lg {
+ flex: 3 1 32rem;
+ }
- @media (max-width: 767px) {
+ // members-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%;
}
}
- .representatives {
+ .supporters-section {
display: flex;
flex-direction: column;
- align-items: center;
- flex: 1;
-
- h3 {
- text-align: center;
+ 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;
}
- ul {
- padding-inline: 0;
- width: 100%;
+ &--platinum {
+ --tier-color: var(--color-tier-platinum);
+ --tier-badge-color: var(--color-tier-platinum-badge);
}
- li {
- list-style: none;
- text-align: center;
- background-color: #fafafa;
- border-radius: 8px;
- margin-block: .5em;
- padding: .5em 1rem;
+ &--gold {
+ --tier-color: var(--color-tier-gold);
+ --tier-badge-color: var(--color-tier-gold-badge);
}
- }
-
- .tiers-row {
- display: grid;
- grid-template-columns: repeat(4, 15rem);
- gap: 1rem;
- overflow-y: auto;
- padding-block-start: 1.5em;
- margin-block-end: 2em;
-
- .tier-card {
- border: 1px solid var(--borders-color);
- border-radius: 16px;
- background-color: #fff;
-
- display: flex;
- flex-direction: column;
- padding: 1em;
-
- h3 {
- margin-inline: auto;
- margin-block-start: .5rem;
- }
-
- .pricing {
- background-color: #e9e9e9;
- border-radius: 9999px;
- margin-inline: auto;
- padding-inline: .8em;
- }
- ul {
- margin-block-start: 1rem;
- padding-inline-start: 1em;
-
- li {
- list-style-image: url('/assets/tick_supported.svg');
- margin-block: .3em;
- }
- }
+ &--silver {
+ --tier-color: var(--color-tier-silver);
+ --tier-badge-color: var(--color-tier-silver-badge);
}
- }
-}
-
-#platinum-supporters .supporters-card {
- --cards-per-row: 4;
-}
-
-#gold-supporters .supporters-card {
- --cards-per-row: 4;
-}
-
-#silver-supporters .supporters-card {
- --cards-per-row: 5;
-}
-
-#associate-supporters .supporters-card {
- --cards-per-row: 5;
-}
-
-#ecosystem-supporters .supporters-card {
- --cards-per-row: 5;
-}
-#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);
+ &--neutral {
+ --tier-color: var(--color-border);
+ --tier-badge-color: var(--color-text-muted);
+ }
- .supporters-section {
- display: flex;
- flex-direction: column;
+ // Separate from .supporters-section-flag (the decorative corner
+ // flourish, which scrolls away with the section) - this stays
+ // pinned near the top of the viewport while a tall section scrolls
+ // past, so it's still clear which tier's logos are on screen.
+ .supporters-section-sticky-label {
+ align-self: flex-start;
+ position: sticky;
+ top: calc(var(--navbar-height) + 0.5rem);
+ z-index: 2;
+ margin-bottom: 0.75rem;
+ padding: 0.2rem 0.7rem;
+ border-radius: 9999px;
+ background-color: var(--tier-badge-color);
+ color: #fff;
+ font-size: 0.75rem;
+ font-weight: 700;
+
+ // Hidden until JS (membership.html) marks the section as
+ // .is-stuck, i.e. the corner flag has scrolled out of view -
+ // otherwise this sits right next to that flag and just reads
+ // as a duplicate label.
+ opacity: 0;
+ pointer-events: none;
+ transition: opacity 0.15s ease;
+ }
- h2 {
- text-align: center;
+ &.is-stuck .supporters-section-sticky-label {
+ opacity: 1;
+ pointer-events: auto;
}
.cards {
@@ -166,14 +192,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 +211,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..14acc7c443 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;
@@ -21,6 +25,12 @@
@media (max-width: 767px) {
padding: 1rem;
+ // The sticky header's real rendered height on mobile is taller
+ // than the site-wide scroll-padding-top (_base.scss) accounts
+ // for, so an anchor jump lands with the heading tucked behind
+ // it. Desktop's header is shorter and the site-wide value
+ // already covers it fine, so this stays mobile-only.
+ scroll-margin-top: 4.625rem;
}
&.coloured {
diff --git a/sass/style.scss b/sass/style.scss
index 93e0813b06..1907898c0f 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';
@@ -21,6 +22,7 @@
@import '_security-hall-of-fame';
@import '_hosting';
@import '_try-matrix';
+@import '_membership';
@import '_support';
@import '_howitworks';
@import '_elections';
diff --git a/static/js/members-masonry.js b/static/js/members-masonry.js
new file mode 100644
index 0000000000..61f19239ec
--- /dev/null
+++ b/static/js/members-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..c695d3d150 100644
--- a/templates/membership.html
+++ b/templates/membership.html
@@ -1,20 +1,79 @@
{% 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.
+
- Working groups are where you can help get things done. Having a
+ Working Groups are where you can help get things done. Having a
direction is great, having hands to implement it is even better.
@@ -75,18 +101,20 @@
Join a working group
Everywhere!
- Joining a working group is easy: Just pick one and join
+ Joining a Working Group is easy: Just pick one and join
their chat room!