Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
73 changes: 73 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,46 @@ The RadicalxChange www site is built on [11ty](https://www.11ty.dev/). It's styl

## Design

> A refinement effort is tightening the site's craft _within_ the existing brand
> (hierarchy, spacing, component consistency, restraint). See `DESIGN_DIRECTION.md`
> for the audit and plan. The tokens below are the shared foundation that work builds on.

### Palette

Three brand colors, nothing else:

| Token | Hex | Role |
| ---------------- | --------- | ----------------------------------------------------------- |
| `black` | `#000000` | Primary ink; black section bands ("the big moments"). |
| `white` | `#FFFFFF` | Default page surface. |
| `golden-fizz` | `#EDFF38` | Brand **yellow** — punctuation only. |

**Yellow is punctuation, not wallpaper.** Use it for accents (underlines, icon
circles, highlight marks, number circles, a single key callout) and the rare,
countable full-bleed jolt (newsletter, donate CTA, 404). Reach for **black
sections** — not yellow fills — for big confident moments. Default surface is white.

**Contrast rule (ADA), from the Brand Book:** black-on-yellow, black-on-white, or
white/yellow-on-black only — **never** yellow/white on yellow/white, and never
yellow text on white.

`gray` and `red` remain in the config but are **restricted, non-brand** colors kept
only for functional data-viz on the Community Week NYC microsite (pass / disagree vote
bars). **They are retained intentionally — please don't "tidy them up."** There the
colors carry _meaning_ (data encoding), not decoration, so they're a different category
from the brand surface and removing them would break the vote bars. The brand rule
(black / white / yellow) governs brand surfaces; a data-viz legend is exempt. Revisit
the encoding only if/when CWNYC gets a visual refresh on its own merits. Don't use them
on brand surfaces, and don't introduce new usages.

### Surface & color scheme

`global.css` sets `color-scheme: light` and a white `body` background so no page can
fall through to the browser's dark canvas (which previously made black text invisible
in OS dark mode). These live on low-specificity selectors, so any utility
(`bg-black`, `bg-golden-fizz`, a page's `bodyBg`) still overrides them — intentionally
dark or yellow bands are unaffected.

### Grid

Pages are laid out on grids. A grid has fixed-width margins, fixed-width gutters and fluid columns. Desktop pages are on a 12-column or 16-column grid. Mobile pages are on a 4-column grid.
Expand All @@ -48,12 +88,45 @@ We use TailWind's built-in scale for general size and space. These utilities use

For typography heavy pages, we use a multiple of line height for vertical spacing. The utilities use `em` units.

#### Section rhythm

For the gaps **between** structural blocks, prefer the documented rhythm tokens over
ad-hoc `mb-16` / `mb-8` one-offs, so every page shares one calm cadence:

| Token | Value | Use |
| --------- | ------------------------ | ------------------------------------- |
| `section` | `clamp(4rem, 8vw, 8rem)` | Between major page sections. |
| `block` | `clamp(2rem, 4vw, 3rem)` | Between blocks within a section. |
| `element` | `1.5rem` | Between elements within a block. |

Apply as any Tailwind spacing utility, e.g. `mb-section`, `space-y-block`, `py-section`,
`gap-element`.

### Typography

We use a [fluid type scale](https://utopia.fyi/blog/designing-with-fluid-type-scales) for almost all text. This scale provides a base text size and "steps" up and down from that. If you assign a step size to text, it will automatically scale relative to the width of the browser.

For text written in Messer font, use font-size in the `vw` unit instead.

#### Type roles

Two families, a small set of roles. **Only one Messer weight is licensed for web**
(`MesserV2.0-Condensed`, 400) — so display hierarchy comes from **size, not weight**.
Keep to these roles rather than inventing per-page sizes:

| Role | Font (`font-…`) | Treatment | Notes |
| ------------------- | ---------------------- | ------------------------------------------- | ------------------------------------------------ |
| Display / page title | `display` (Messer) | **ALL CAPS**, `size-lg/display`+ | One per page. Caps only — Messer is a caps face. |
| Hero statement | `display` (Messer caps) | short, oversized | Keep it short (≈4–8 words); long copy → standfirst. |
| Section header | `body` bold (Suisse) | `size-2`, with a hairline rule beneath | Pairs with the eyebrow below. |
| Eyebrow / kicker | `body` bold (Suisse) | uppercase, `size--1`, letter-spaced, quiet | A small label, **not** a headline. |
| Body | `body` (Suisse) | `size-0` | Brand body face is Suisse Book.¹ |
| Meta (author/date/tag) | `body` (Suisse) | `size--1` | One consistent size for all metadata. |

¹ The Brand Book specifies **Suisse Int'l Book** for paragraph text; today `font-body`
resolves to Suisse Int'l _Regular_. A small, deliberate drift to reconcile in a later
typography pass — flagged, not silently changed.

## Code Layout

11ty builds from _src/site_. Read 11ty's documentation to understand a lot about this directory. We outline only notable quirks here:
Expand Down
37 changes: 37 additions & 0 deletions src/site/_includes/components/button.njk
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
<!-- prettier-ignore -->
{#
Buttons — two weights, one rhythm.

primary — pill with sliding arrow. The page's main call to action.
(Matches the existing action-button so adoption is a rename.)
secondary — uppercase text + thick underline. For lower-priority actions
and "see all"-style links that aren't a section header.

Use at most one primary per view; reach for secondary otherwise.
#}
{% macro primary(href, label) %}
<a
class="group inline-flex items-center justify-between py-2 px-4 xl:pl-6 xl:pr-4 border border-black rounded-oval text-size--1"
href="{{ href }}"
>
<span class="text-center uppercase">{{ label }}</span>
<span
class="hidden xl:inline w-4 group-hover:w-12 transition-width duration-300 ease-out"
></span>
<img
class="hidden xl:inline-block"
src="/images/ui/next-arrow.svg"
width="24"
height="18"
alt=""
/>
</a>
{% endmacro %}

{% macro secondary(href, label) %}
<a
class="group inline-flex items-baseline gap-2 text-size--1 uppercase tracking-wide thick-link"
href="{{ href }}"
>{{ label }} <span aria-hidden="true">&rarr;</span></a
>
{% endmacro %}
43 changes: 43 additions & 0 deletions src/site/_includes/components/content-card.njk
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
<!-- prettier-ignore -->
{#
Content card — the ONE canonical card, promoted from the (good) Updates
card and made uniform-height without the old min-height hack.

Structure: tag · title · optional description · meta (author / date),
pinned to the bottom so meta aligns across a row. The whole card is the
link. Drop it in any CSS grid; `h-full` + the grid's stretch makes every
card in a row equal height, and `mt-auto` keeps the meta on the baseline.

Args:
href (required)
title (required)
tag (optional) — small uppercase label (e.g. "Blog Post", "Project")
description (optional) — short standfirst (used by Projects/Proposals cards)
meta1 (optional) — e.g. author
meta2 (optional) — e.g. date
#}
{% macro render(href, title, tag, description, meta1, meta2) %}
<a
href="{{ href }}"
class="group flex flex-col h-full pt-element border-t border-black"
>
<!-- prettier-ignore -->
{% if tag %}
<p class="mb-element text-size--1 uppercase tracking-wide opacity-60">
{{ tag }}
</p>
{% endif %}
<h3 class="text-size-2 leading-tight group-hover:thick-link">{{ title }}</h3>
<!-- prettier-ignore -->
{% if description %}
<p class="mt-element text-size--1 opacity-80">{{ description }}</p>
{% endif %}
<!-- prettier-ignore -->
{% if meta1 or meta2 %}
<div class="mt-auto pt-block text-size--1 opacity-60">
{% if meta1 %}<p>{{ meta1 }}</p>{% endif %}
{% if meta2 %}<p>{{ meta2 }}</p>{% endif %}
</div>
{% endif %}
</a>
{% endmacro %}
2 changes: 1 addition & 1 deletion src/site/_includes/components/popup.njk
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@
<div
class="sticky bottom-0 left-0 lg:px-16 lg:rounded-tvinfo items-center"
>
<button class="block bg-light-gold rounded-oval py-3 px-8 mx-auto mb-4 mt-12">
<button class="block bg-golden-fizz rounded-oval py-3 px-8 mx-auto mb-4 mt-12">
<a
class="text-black text-size--2 uppercase"
href="{{ link }}"
Expand Down
34 changes: 34 additions & 0 deletions src/site/_includes/components/section-header.njk
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
<!-- prettier-ignore -->
{#
Section header — the shared "section header + rule + See all" motif.

Every listing and homepage section opens with this so the page has one
consistent rhythm instead of ad-hoc per-section headings.

Args:
title (required) — section title, Suisse Bold
eyebrow (optional) — small uppercase kicker above the title
seeAllHref (optional) — if set, renders a right-aligned "See all →"
seeAllLabel (optional) — overrides the "See all" text
#}
{% macro render(title, eyebrow, seeAllHref, seeAllLabel) %}
<div class="border-b border-black pb-element">
<!-- prettier-ignore -->
{% if eyebrow %}
<p class="mb-element text-size--1 font-bold uppercase tracking-wide">
{{ eyebrow }}
</p>
{% endif %}
<div class="flex items-baseline justify-between gap-element">
<h2 class="font-bold text-size-2">{{ title }}</h2>
<!-- prettier-ignore -->
{% if seeAllHref %}
<a
href="{{ seeAllHref }}"
class="shrink-0 text-size--1 uppercase tracking-wide thick-link"
>{{ seeAllLabel or "See all" }} &rarr;</a
>
{% endif %}
</div>
</div>
{% endmacro %}
54 changes: 54 additions & 0 deletions src/site/_includes/css/components.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
/* Reusable component motifs.

This is where the brand YELLOW lives — as punctuation, not wallpaper.
Keep these small and composable; pages assemble them, they don't restyle
them. See README → Design, and the /styleguide page for live examples. */

/* Yellow highlight behind a word or phrase (inline). Black-on-yellow = ADA-safe. */
.mark-highlight {
background-color: theme("colors.golden-fizz");
box-decoration-break: clone;
-webkit-box-decoration-break: clone;
padding: 0 0.12em;
}

/* Thick yellow underline sitting behind the text baseline. */
.underline-yellow {
background-image: linear-gradient(
theme("colors.golden-fizz"),
theme("colors.golden-fizz")
);
background-repeat: no-repeat;
background-position: 0 88%;
background-size: 100% 0.32em;
}

/* Circle holding a glyph/icon — yellow fill, black mark (ADA-safe). */
.icon-circle {
display: inline-flex;
align-items: center;
justify-content: center;
width: 2.5em;
height: 2.5em;
border-radius: 9999px;
background-color: theme("colors.golden-fizz");
color: theme("colors.black");
}

/* Numbered step marker — outlined circle, used for ordered sequences. */
.number-circle {
display: inline-flex;
align-items: center;
justify-content: center;
width: 2.2em;
height: 2.2em;
border-radius: 9999px;
border: 2px solid theme("colors.black");
font-variant-numeric: tabular-nums;
line-height: 1;
}

/* Inside a black band (.is-on-dark), flip the outlined motifs to read on black. */
.is-on-dark .number-circle {
border-color: theme("colors.white");
}
20 changes: 20 additions & 0 deletions src/site/_includes/css/global.css
Original file line number Diff line number Diff line change
@@ -1,3 +1,23 @@
/* Lock the site to a light surface.

Without this, pages that don't paint their own background fall through to the
browser's default canvas — which is BLACK for visitors whose OS is in dark
mode, rendering black body text invisible (Updates, articles, Search, Wiki…).
`color-scheme: light` also keeps native controls and scrollbars light.

This is a DEFAULT, not an override: the background/color live on the low-
specificity `body` element selector, so any Tailwind utility on a page
wrapper or section (bg-black, bg-golden-fizz, text-white, or a page's
`bodyBg`) still wins. Intentionally-dark bands are unaffected. */
:root {
color-scheme: light;
}

html {
scroll-behavior: smooth;
}

body {
background-color: theme("colors.white");
color: theme("colors.black");
}
6 changes: 4 additions & 2 deletions src/site/_includes/css/link.css
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
.see-more-link {
@apply text-gray;
/* De-emphasized via opacity on brand black rather than an off-brand grey. */
color: theme("colors.black");
opacity: 0.6;

&:hover {
@apply text-black;
opacity: 1;
}
}

Expand Down
1 change: 1 addition & 0 deletions src/site/_includes/css/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
@import "tailwindcss/components";

/* Custom components */
@import "./components.css";
@import "./logo.css";
@import "./menu.css";
@import "./about.css";
Expand Down
2 changes: 1 addition & 1 deletion src/site/about/index.njk
Original file line number Diff line number Diff line change
Expand Up @@ -234,7 +234,7 @@ supporters:
<p>{{ article.date | readableDate }}</p></a
>
<div
class="about_press-description z-10 absolute top-1/2 left-1/8 w-max p-2 bg-light-black text-white text-size--4"
class="about_press-description z-10 absolute top-1/2 left-1/8 w-max p-2 bg-black text-white text-size--4"
>
{{ article.title }}
</div>
Expand Down
Loading