Browse Source

Establish cascade-layer CSS architecture and self-host theme typography

Adopts a build-free modern CSS stack per todo.md: six cascade layers
(reset → tokens → base → layout → components → utilities), modern-normalize
in the reset layer, hand-rolled design tokens with an Open Props
cherry-pick workflow, and self-hosted Adelle (serif, headings) plus
Open Sans (variable, body) fonts. Existing starterkit component CSS is
wrapped in @layer components; the previously-misnamed `base` library is
renamed to `components`; YAML config files and architectural CSS files
gain explanatory headers; CLAUDE.md documents the full setup so future
work can pick it up cold.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
master
rdrew 2 weeks ago
parent
commit
7ec36976cb
  1. 74
      CLAUDE.md
  2. 29
      css/base.css
  3. 4
      css/components/action-links.css
  4. 4
      css/components/breadcrumb.css
  5. 4
      css/components/button.css
  6. 4
      css/components/container-inline.css
  7. 4
      css/components/details.css
  8. 4
      css/components/dialog.css
  9. 4
      css/components/dropbutton.css
  10. 4
      css/components/exposed-filters.css
  11. 4
      css/components/field.css
  12. 4
      css/components/file.css
  13. 4
      css/components/form.css
  14. 4
      css/components/icons.css
  15. 4
      css/components/image-widget.css
  16. 4
      css/components/indented.css
  17. 4
      css/components/inline-form.css
  18. 4
      css/components/item-list.css
  19. 4
      css/components/link.css
  20. 4
      css/components/links.css
  21. 4
      css/components/menu.css
  22. 4
      css/components/messages.css
  23. 4
      css/components/more-link.css
  24. 4
      css/components/node.css
  25. 4
      css/components/pager.css
  26. 4
      css/components/progress.css
  27. 4
      css/components/search-results.css
  28. 4
      css/components/tabledrag.css
  29. 4
      css/components/tableselect.css
  30. 4
      css/components/tablesort.css
  31. 4
      css/components/tabs.css
  32. 4
      css/components/textarea.css
  33. 4
      css/components/ui-dialog.css
  34. 4
      css/components/user.css
  35. 120
      css/fonts.css
  36. 16
      css/layers.css
  37. 22
      css/layout.css
  38. 243
      css/lib/modern-normalize.css
  39. 69
      css/tokens.css
  40. 27
      css/utilities.css
  41. 22
      druid.info.yml
  42. 63
      druid.libraries.yml
  43. BIN
      fonts/adelle/Adelle_Bold.otf
  44. BIN
      fonts/adelle/Adelle_Bold.woff2
  45. BIN
      fonts/adelle/Adelle_BoldItalic.otf
  46. BIN
      fonts/adelle/Adelle_BoldItalic.woff2
  47. BIN
      fonts/adelle/Adelle_ExtraBold.otf
  48. BIN
      fonts/adelle/Adelle_ExtraBold.woff2
  49. BIN
      fonts/adelle/Adelle_ExtraBoldItalic.otf
  50. BIN
      fonts/adelle/Adelle_ExtraBoldItalic.woff2
  51. BIN
      fonts/adelle/Adelle_Heavy.otf
  52. BIN
      fonts/adelle/Adelle_Heavy.woff2
  53. BIN
      fonts/adelle/Adelle_HeavyItalic.otf
  54. BIN
      fonts/adelle/Adelle_HeavyItalic.woff2
  55. BIN
      fonts/adelle/Adelle_Italic.otf
  56. BIN
      fonts/adelle/Adelle_Italic.woff2
  57. BIN
      fonts/adelle/Adelle_LightItalic.otf
  58. BIN
      fonts/adelle/Adelle_LightItalic.woff2
  59. BIN
      fonts/adelle/Adelle_Reg.otf
  60. BIN
      fonts/adelle/Adelle_Reg.woff2
  61. BIN
      fonts/adelle/Adelle_SemiBoldItalic.otf
  62. BIN
      fonts/adelle/Adelle_SemiBoldItalic.woff2
  63. BIN
      fonts/adelle/Adelle_Semibold.otf
  64. BIN
      fonts/adelle/Adelle_Semibold.woff2
  65. BIN
      fonts/adelle/Adelle_light.otf
  66. BIN
      fonts/adelle/Adelle_light.woff2
  67. BIN
      fonts/open-sans/OpenSans-Italic-Latin.woff2
  68. BIN
      fonts/open-sans/OpenSans-Italic-LatinExt.woff2
  69. BIN
      fonts/open-sans/OpenSans-Latin.woff2
  70. BIN
      fonts/open-sans/OpenSans-LatinExt.woff2
  71. 210
      todo.md

74
CLAUDE.md

@ -0,0 +1,74 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## What this is
`druid` is a Drupal 11 custom theme generated from core's `starterkit_theme` (generator: `starterkit_theme:11.3.7`). It declares `stable9` as its base theme in `druid.info.yml`, so any template not overridden here falls back to `stable9`'s version. The theme lives at `web/themes/custom/druid` inside a larger Drupal site (sibling theme `orion` lives next to it; site-level `composer.json` is at the repo root).
There is no build pipeline, no test suite, no linter, and no preprocessor in this directory. CSS is hand-authored modern CSS (custom properties, cascade layers, nesting, container queries, `:where()`, `color-mix()`, etc.) and consumed directly by Drupal. "Building" the theme means clearing Drupal's cache so it picks up `.yml` and template changes — CSS *content* edits need no rebuild. See `todo.md` for the design rationale that informed this stack choice (no Sass, no PostCSS, no Pico).
## CSS architecture: cascade layers
This is the central architectural decision and everything below it depends on understanding it.
**Layer order**, declared once in `css/layers.css` and authoritative for the whole theme:
```css
@layer reset, tokens, base, layout, components, utilities;
```
Later layers win against earlier ones regardless of selector specificity. Each layer is filled by exactly one file (except `components`, which is split per UI element), and each file is registered as its own library so loading is deterministic:
| Layer | File(s) | Library | SMACSS cat. | Weight |
| ------------ | -------------------------------- | ------------------ | ----------- | ------ |
| (order decl) | `css/layers.css` | `druid/layers` | `base` | -100 |
| reset | `css/lib/modern-normalize.css` | `druid/reset` | `base` | -90 |
| (font-face) | `css/fonts.css` | `druid/fonts` | `base` | -85 |
| tokens | `css/tokens.css` | `druid/tokens` | `base` | -80 |
| base | `css/base.css` | `druid/base` | `base` | -70 |
| layout | `css/layout.css` | `druid/layout` | `layout` | -60 |
| components | `css/components/*.css` (32 files)| `druid/components` | `component` | -10/file |
| utilities | `css/utilities.css` | `druid/utilities` | `theme` | -50 |
All eight libraries are attached on every page via `druid.info.yml` `libraries:` in cascade order. **Do not remove or reorder that list** — the layer-order declaration in `layers.css` must parse before any `@layer foo { … }` block, or the order becomes whatever Drupal happens to load first.
`css/fonts.css` is the one architecture file that intentionally lives *outside* any `@layer` block. `@font-face` declarations register font resources rather than cascade rules — they don't compete in the cascade, so layer membership is irrelevant. The font files themselves live in `fonts/<family>/` (e.g. `fonts/adelle/`, `fonts/open-sans/`).
**Critical footgun: unlayered CSS beats all layered CSS.** Any rule outside a `@layer { … }` block silently wins against everything inside layers — this is per the CSS spec, not a bug. Every CSS file in this theme is wrapped; any new rule you author must go *inside* the appropriate `@layer NAME { … }` block, never alongside it.
**`!important` inverts layer order.** An `!important` declaration in an *earlier* layer beats `!important` in a later layer. Avoid `!important` inside `@layer utilities` — the layer is already designed to win.
### Where to add a new CSS rule
| Kind of rule | Goes in |
| --------------------------------------------------------------------- | -------------------------------------------------- |
| A `--custom-property` definition (color, size, font, radius, etc.) | `css/tokens.css` |
| A new `@font-face` / web font | `css/fonts.css` (+ woff2 file in `fonts/<family>/`)|
| Bare element selector (`body`, `a`, `h1`, `ul`, …) | `css/base.css` |
| Region/grid/container arrangement (`.layout-container`, page shell) | `css/layout.css` |
| A discrete UI widget (button, card, menu, form control, Drupal block) | `css/components/<thing>.css` |
| Single-purpose helper (`.text-center`, `.mt-4`, print toggles) | `css/utilities.css` |
| A browser default override | `css/lib/modern-normalize.css`* |
*Only when re-vendoring a new modern-normalize release — see the file header for the workflow.
### Tokens / Open Props
`css/tokens.css` holds CSS custom properties on `:root`. We do **not** import the full Open Props library — instead, we cherry-pick individual tokens by copying their declarations into this file. The workflow is documented in the file's header comment; re-read it before adding tokens. The short version: browse https://open-props.style, copy the `--prop: value;` line, paste into the relevant section, preserve the Open Props name.
## Other layout that matters
- `druid.info.yml` — declares base theme, always-on libraries (one per cascade layer), and `libraries-extend` entries that attach extra component CSS only when the corresponding core library is in use (e.g. `core/drupal.dialog``druid/dialog`). The `libraries-extend` libraries (`dialog`, `dropbutton`, `file`, `item-list`, `progress`, `drupal.tablesort`, `user`) are separate from the always-on `components` library because they target widgets that don't appear on every page.
- `druid.libraries.yml` — every CSS library. New component CSS files added to `css/components/` need a corresponding entry under the `components:` library (or their own library if they should be conditional via `libraries-extend`).
- `druid.theme``hook_preprocess_HOOK` functions. Currently only `druid_preprocess_image_widget()`, which strips the preview container when the user lacks access.
- `templates/` — Twig overrides organized by Drupal's own taxonomy (`block/`, `content/`, `content-edit/`, `dataset/`, `field/`, `form/`, `layout/`, `misc/`, `navigation/`, `user/`, `views/`). Filenames follow Drupal's theme-hook-suggestion convention (`field--node--title.html.twig`, `block--system-menu-block.html.twig`, etc.) — renaming them breaks the override.
## Working in this theme
- **Cache clear**: required after any `.yml`, `.theme`, or template change — `drush cr` from the site root. CSS *content* edits do not require it; only adding/removing files or library entries does.
- **New component CSS**: create the file in `css/components/`, wrap its rules in `@layer components { … }`, register it under the `components:` library in `druid.libraries.yml` with `weight: -10`. If the styles should only load when a specific core library is in use, give it its own library and wire it via `libraries-extend` in `druid.info.yml` instead.
- **New Twig override**: copy the upstream template from `core/themes/stable9/templates/…` (or the originating module) into the matching subdirectory here — don't write from scratch; the docblock comments document available variables and are worth keeping.
- **New preprocess hook**: follow the existing signature in `druid.theme` (`array &$variables`, `: void` return type, `/** Implements hook_X(). */` docblock).
- **Drupal core / contrib CSS is unlayered.** That means it wins against any `@layer components` rule you write — this is the one place the architecture leaks. Options: bump specificity for the override, or wrap the offending core CSS via a future `hook_css_alter` (not currently implemented). Don't reach for `!important` reflexively.
- The repo is generated from a starter kit; upstream Drupal docs at https://www.drupal.org/docs/core-modules-and-themes/core-themes/starterkit-theme are the canonical reference for regeneration and theme-API conventions.

29
css/base.css

@ -0,0 +1,29 @@
/**
* @file
* Element-level defaults for the druid theme.
*
* Bare element selectors only (body, headings, links, lists, etc.). For
* discrete UI components, see css/components/. For page-level structure,
* see css/layout.css. Every rule lives in the `base` cascade layer so it
* loses cleanly to anything in layout / components / utilities.
*/
@layer base {
body {
background: var(--surface-1);
color: var(--text-1);
font-family: var(--font-sans);
line-height: 1.5;
}
/* Headings use the serif token (Adelle). Drupal core inserts <h1><h6>
in unpredictable places (admin toolbars, contextual links, system
blocks), so this targets bare heading elements re-declare in a
component or utility if a specific UI surface needs the sans family. */
h1, h2, h3, h4, h5, h6 {
font-family: var(--font-serif);
}
a {
color: var(--brand);
}
}

4
css/components/action-links.css

@ -1,3 +1,5 @@
@layer components {
/** /**
* @file * @file
* Styles for link buttons and action links. * Styles for link buttons and action links.
@ -41,3 +43,5 @@
padding-right: 0; padding-right: 0;
padding-left: 0.2em; padding-left: 0.2em;
} }
}

4
css/components/breadcrumb.css

@ -1,3 +1,5 @@
@layer components {
/** /**
* @file * @file
* Styles for breadcrumbs. * Styles for breadcrumbs.
@ -27,3 +29,5 @@
.breadcrumb li:first-child::before { .breadcrumb li:first-child::before {
content: none; content: none;
} }
}

4
css/components/button.css

@ -1,3 +1,5 @@
@layer components {
/** /**
* @file * @file
* Visual styles for buttons. * Visual styles for buttons.
@ -13,3 +15,5 @@
margin-right: 0; margin-right: 0;
margin-left: 0; margin-left: 0;
} }
}

4
css/components/container-inline.css

@ -1,3 +1,5 @@
@layer components {
/** /**
* @file * @file
* Inline items. * Inline items.
@ -20,3 +22,5 @@
margin-top: 0; margin-top: 0;
margin-bottom: 0; margin-bottom: 0;
} }
}

4
css/components/details.css

@ -1,3 +1,5 @@
@layer components {
/** /**
* @file * @file
* Collapsible details. * Collapsible details.
@ -18,3 +20,5 @@ summary {
padding: 0.2em 0.5em; padding: 0.2em 0.5em;
cursor: pointer; cursor: pointer;
} }
}

4
css/components/dialog.css

@ -1,3 +1,5 @@
@layer components {
/** /**
* @file * @file
* Presentational styles for Drupal dialogs. * Presentational styles for Drupal dialogs.
@ -71,3 +73,5 @@
.ui-dialog .ajax-progress-throbber .message { .ui-dialog .ajax-progress-throbber .message {
display: none; display: none;
} }
}

4
css/components/dropbutton.css

@ -1,3 +1,5 @@
@layer components {
/** /**
* @file * @file
* General styles for dropbuttons. * General styles for dropbuttons.
@ -31,3 +33,5 @@
margin-right: 0; margin-right: 0;
margin-left: 0.25em; margin-left: 0.25em;
} }
}

4
css/components/exposed-filters.css

@ -1,3 +1,5 @@
@layer components {
/** /**
* @file * @file
* Visual styles for exposed filters. * Visual styles for exposed filters.
@ -44,3 +46,5 @@
margin-right: 0; margin-right: 0;
margin-left: 1em; margin-left: 1em;
} }
}

4
css/components/field.css

@ -1,3 +1,5 @@
@layer components {
/** /**
* @file * @file
* Visual styles for fields. * Visual styles for fields.
@ -23,3 +25,5 @@
.field--label-inline .field__label::after { .field--label-inline .field__label::after {
content: ":"; content: ":";
} }
}

4
css/components/file.css

@ -1,3 +1,5 @@
@layer components {
/** /**
* @file * @file
* Default style for file module. * Default style for file module.
@ -60,3 +62,5 @@
.file--image { .file--image {
background-image: url(../../images/icons/image-x-generic.png); background-image: url(../../images/icons/image-x-generic.png);
} }
}

4
css/components/form.css

@ -1,3 +1,5 @@
@layer components {
/** /**
* @file * @file
* Visual styles for form components. * Visual styles for form components.
@ -102,3 +104,5 @@ abbr.ajax-changed {
background: url(../../images/icons/error.svg) no-repeat; background: url(../../images/icons/error.svg) no-repeat;
background-size: contain; background-size: contain;
} }
}

4
css/components/icons.css

@ -1,3 +1,5 @@
@layer components {
/** /**
* @file * @file
* Visual styles for icons. * Visual styles for icons.
@ -19,3 +21,5 @@
text-indent: -9999px; text-indent: -9999px;
background: url(../../images/icons/feed.svg) no-repeat; background: url(../../images/icons/feed.svg) no-repeat;
} }
}

4
css/components/image-widget.css

@ -1,3 +1,5 @@
@layer components {
/** /**
* @file * @file
* Image upload widget. * Image upload widget.
@ -20,3 +22,5 @@
.image-widget-data .text-field { .image-widget-data .text-field {
width: auto; width: auto;
} }
}

4
css/components/indented.css

@ -1,3 +1,5 @@
@layer components {
/** /**
* @file * @file
* Basic styling for comment module. * Basic styling for comment module.
@ -13,3 +15,5 @@
margin-right: 25px; margin-right: 25px;
margin-left: 0; margin-left: 0;
} }
}

4
css/components/inline-form.css

@ -1,3 +1,5 @@
@layer components {
/** /**
* @file * @file
* Visual styles for inline forms. * Visual styles for inline forms.
@ -31,3 +33,5 @@
[dir="rtl"] .form--inline .form-actions { [dir="rtl"] .form--inline .form-actions {
clear: right; clear: right;
} }
}

4
css/components/item-list.css

@ -1,3 +1,5 @@
@layer components {
/** /**
* @file * @file
* Visual styles for item list. * Visual styles for item list.
@ -30,3 +32,5 @@
[dir="rtl"] .item-list__comma-list li { [dir="rtl"] .item-list__comma-list li {
margin: 0; margin: 0;
} }
}

4
css/components/link.css

@ -1,3 +1,5 @@
@layer components {
/** /**
* @file * @file
* Style another element as a link. * Style another element as a link.
@ -14,3 +16,5 @@ button.link {
label button.link { label button.link {
font-weight: bold; font-weight: bold;
} }
}

4
css/components/links.css

@ -1,3 +1,5 @@
@layer components {
/** /**
* @file * @file
* Visual styles for links. * Visual styles for links.
@ -21,3 +23,5 @@ ul.inline li {
ul.links a.is-active { ul.links a.is-active {
color: #000; color: #000;
} }
}

4
css/components/menu.css

@ -1,3 +1,5 @@
@layer components {
/** /**
* @file * @file
* Visual styles for menu. * Visual styles for menu.
@ -32,3 +34,5 @@ ul.menu {
ul.menu a.is-active { ul.menu a.is-active {
color: #000; color: #000;
} }
}

4
css/components/messages.css

@ -1,3 +1,5 @@
@layer components {
/** /**
* @file * @file
* Styles for system messages. * Styles for system messages.
@ -67,3 +69,5 @@
.messages--error p.error { .messages--error p.error {
color: #a51b00; color: #a51b00;
} }
}

4
css/components/more-link.css

@ -1,3 +1,5 @@
@layer components {
/** /**
* @file * @file
* Markup generated by #type 'more_link'. * Markup generated by #type 'more_link'.
@ -10,3 +12,5 @@
[dir="rtl"] .more-link { [dir="rtl"] .more-link {
text-align: left; text-align: left;
} }
}

4
css/components/node.css

@ -1,3 +1,5 @@
@layer components {
/** /**
* @file * @file
* Visual styles for nodes. * Visual styles for nodes.
@ -6,3 +8,5 @@
.node--unpublished { .node--unpublished {
background-color: #fff4f4; background-color: #fff4f4;
} }
}

4
css/components/pager.css

@ -1,3 +1,5 @@
@layer components {
/** /**
* @file * @file
* Visual styles for pager. * Visual styles for pager.
@ -14,3 +16,5 @@
.pager__item.is-active { .pager__item.is-active {
font-weight: bold; font-weight: bold;
} }
}

4
css/components/progress.css

@ -1,3 +1,5 @@
@layer components {
/** /**
* @file * @file
* Visual styles for progress bar. * Visual styles for progress bar.
@ -56,3 +58,5 @@
-80px 0; -80px 0;
} }
} }
}

4
css/components/search-results.css

@ -1,3 +1,5 @@
@layer components {
/** /**
* @file * @file
* Stylesheet for results generated by the Search module. * Stylesheet for results generated by the Search module.
@ -6,3 +8,5 @@
.search-results { .search-results {
list-style: none; list-style: none;
} }
}

4
css/components/tabledrag.css

@ -1,3 +1,5 @@
@layer components {
/** /**
* @file * @file
* Visual styles for table drag. * Visual styles for table drag.
@ -12,3 +14,5 @@ tr.drag-previous {
body div.tabledrag-changed-warning { body div.tabledrag-changed-warning {
margin-bottom: 0.5em; margin-bottom: 0.5em;
} }
}

4
css/components/tableselect.css

@ -1,3 +1,5 @@
@layer components {
/** /**
* @file * @file
* Table select behavior. * Table select behavior.
@ -17,3 +19,5 @@ th.checkbox {
/* This is required to win over specificity of [dir="rtl"] td */ /* This is required to win over specificity of [dir="rtl"] td */
text-align: center; text-align: center;
} }
}

4
css/components/tablesort.css

@ -1,3 +1,5 @@
@layer components {
/** /**
* @file * @file
* Table sort indicator. * Table sort indicator.
@ -9,3 +11,5 @@ th.is-active img {
td.is-active { td.is-active {
background-color: #ddd; background-color: #ddd;
} }
}

4
css/components/tabs.css

@ -1,3 +1,5 @@
@layer components {
/** /**
* @file * @file
* Visual styles for tabs. * Visual styles for tabs.
@ -31,3 +33,5 @@ ul.tabs {
.tabs a:hover { .tabs a:hover {
background-color: #f5f5f5; background-color: #f5f5f5;
} }
}

4
css/components/textarea.css

@ -1,3 +1,5 @@
@layer components {
/** /**
* @file * @file
* Visual styles for a resizable textarea. * Visual styles for a resizable textarea.
@ -9,3 +11,5 @@
width: 100%; width: 100%;
margin: 0; margin: 0;
} }
}

4
css/components/ui-dialog.css

@ -1,3 +1,5 @@
@layer components {
/** /**
* @file * @file
* Styles for modal windows. * Styles for modal windows.
@ -13,3 +15,5 @@
max-width: 95%; max-width: 95%;
} }
} }
}

4
css/components/user.css

@ -1,3 +1,5 @@
@layer components {
/** /**
* @file * @file
* Theme styling for user module. * Theme styling for user module.
@ -65,3 +67,5 @@
color: #a51b00; color: #a51b00;
font-weight: bold; font-weight: bold;
} }
}

120
css/fonts.css

@ -0,0 +1,120 @@
/**
* @file
* @font-face declarations for the druid theme.
*
* Two families are registered: Adelle (serif, headings) and Open Sans
* (sans, body). All files are self-hosted under /fonts no external
* requests at runtime. The token mapping (`--font-serif`, `--font-sans`)
* lives in css/tokens.css; rules that *apply* the fonts live in
* css/base.css.
*
* @font-face is intentionally NOT wrapped in a cascade layer. @font-face
* registers a font resource it does not produce cascade rules that
* compete with anything, so layer membership is irrelevant.
*
*
* Adelle
*
*
* Self-hosted from /fonts/adelle/. Four weights/styles registered to keep
* the per-page font payload reasonable: Regular, Italic, Bold, BoldItalic.
* The /fonts/adelle/ folder ships additional weights (Light, Semibold,
* ExtraBold, Heavy, etc.) to enable one, add a matching @font-face
* block below, pointing at the corresponding _.woff2 file.
*
*
* Open Sans
*
*
* Self-hosted from /fonts/open-sans/. Each .woff2 is a VARIABLE font
* a single file covers the full weight range (300 to 800), so we declare
* the weight as a range and let the browser pick a weight per element.
*
* Two unicode-range subsets are registered per style: `latin` for English
* and standard western glyphs, `latin-ext` for accented characters,
* Esperanto, etc. The browser only downloads the subset it needs, based
* on the actual code points used on the page.
*
* To re-vendor (or add subsets like cyrillic/greek/vietnamese):
* 1. Visit https://fonts.googleapis.com/css2?family=Open+Sans:ital,wght@0,400;0,700;1,400;1,700&display=swap
* with a modern browser UA (curl -A "Mozilla/5.0 ... Chrome/...").
* 2. Download the woff2 URLs Google returns for the subsets you want.
* 3. Save into /fonts/open-sans/ and add @font-face blocks below
* matching the unicode-range from Google's CSS.
*/
/* Adelle — Regular */
@font-face {
font-family: 'Adelle';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url('../fonts/adelle/Adelle_Reg.woff2') format('woff2');
}
/* Adelle — Italic */
@font-face {
font-family: 'Adelle';
font-style: italic;
font-weight: 400;
font-display: swap;
src: url('../fonts/adelle/Adelle_Italic.woff2') format('woff2');
}
/* Adelle — Bold */
@font-face {
font-family: 'Adelle';
font-style: normal;
font-weight: 700;
font-display: swap;
src: url('../fonts/adelle/Adelle_Bold.woff2') format('woff2');
}
/* Adelle — Bold Italic */
@font-face {
font-family: 'Adelle';
font-style: italic;
font-weight: 700;
font-display: swap;
src: url('../fonts/adelle/Adelle_BoldItalic.woff2') format('woff2');
}
/* Open Sans — Upright, latin */
@font-face {
font-family: 'Open Sans';
font-style: normal;
font-weight: 300 800;
font-display: swap;
src: url('../fonts/open-sans/OpenSans-Latin.woff2') format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
/* Open Sans — Upright, latin-ext */
@font-face {
font-family: 'Open Sans';
font-style: normal;
font-weight: 300 800;
font-display: swap;
src: url('../fonts/open-sans/OpenSans-LatinExt.woff2') format('woff2');
unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* Open Sans — Italic, latin */
@font-face {
font-family: 'Open Sans';
font-style: italic;
font-weight: 300 800;
font-display: swap;
src: url('../fonts/open-sans/OpenSans-Italic-Latin.woff2') format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
/* Open Sans — Italic, latin-ext */
@font-face {
font-family: 'Open Sans';
font-style: italic;
font-weight: 300 800;
font-display: swap;
src: url('../fonts/open-sans/OpenSans-Italic-LatinExt.woff2') format('woff2');
unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}

16
css/layers.css

@ -0,0 +1,16 @@
/**
* @file
* Cascade layer order for the druid theme.
*
* Earlier layers lose to later ones in the cascade, regardless of selector
* specificity. Unlayered CSS beats all layered CSS if you author a new
* stylesheet for this theme, wrap it in the appropriate @layer block.
*
* - reset: modern-normalize and any browser-default resets.
* - tokens: :root custom property definitions (colors, spacing, type).
* - base: element-level defaults (body, headings, links, lists).
* - layout: page-level structural rules (regions, grids, containers).
* - components: discrete UI components and Drupal core overrides.
* - utilities: single-purpose helpers; intentionally wins last.
*/
@layer reset, tokens, base, layout, components, utilities;

22
css/layout.css

@ -0,0 +1,22 @@
/**
* @file
* Page-level structural rules for the druid theme.
*
* This layer holds rules that arrange regions, not rules that style their
* contents grids, flex containers, max-widths, page padding, sidebar
* positioning. If a rule could move from `<header>` to `<aside>` without
* changing meaning, it belongs here; if it's tied to a specific UI element
* (a card, a button, a menu item), put it in css/components/.
*
* Classes referenced by templates/layout/page.html.twig:
* .layout-container outermost wrapper around the whole page
* .layout-content main content column inside <main>
* .layout-sidebar-first optional aside before content
* .layout-sidebar-second optional aside after content
*
* Container queries (@container) are a natural fit for region-internal
* responsiveness see todo.md for the rationale.
*/
@layer layout {
}

243
css/lib/modern-normalize.css vendored

@ -0,0 +1,243 @@
/*
* Vendored copy of modern-normalize for the druid theme.
*
* To re-vendor a new upstream release:
* 1. curl -fsSL https://raw.githubusercontent.com/sindresorhus/modern-normalize/main/modern-normalize.css -o /tmp/mn.css
* 2. Replace everything between `@layer reset {` and the trailing `}`
* below with the contents of /tmp/mn.css.
* 3. Update the version on the banner line just below this comment.
* 4. Commit.
*
* The `@layer reset { }` wrapper is local to this theme; upstream ships
* the file unwrapped. Keeping the reset inside its own cascade layer means
* any rule in tokens / base / layout / components / utilities wins against
* it without needing higher specificity.
*/
/*! modern-normalize v3.0.1 | MIT License | https://github.com/sindresorhus/modern-normalize */
@layer reset {
/*
Document
========
*/
/**
Use a better box model (opinionated).
*/
*,
::before,
::after {
box-sizing: border-box;
}
/**
1. Improve consistency of default fonts in all browsers. (https://github.com/sindresorhus/modern-normalize/issues/3)
2. Correct the line height in all browsers.
3. Prevent adjustments of font size after orientation changes in iOS.
4. Use a more readable tab size (opinionated).
*/
html {
font-family:
system-ui,
'Segoe UI',
Roboto,
Helvetica,
Arial,
sans-serif,
'Apple Color Emoji',
'Segoe UI Emoji'; /* 1 */
line-height: 1.15; /* 2 */
-webkit-text-size-adjust: 100%; /* 3 */
tab-size: 4; /* 4 */
}
/*
Sections
========
*/
/**
Remove the margin in all browsers.
*/
body {
margin: 0;
}
/*
Text-level semantics
====================
*/
/**
Add the correct font weight in Chrome and Safari.
*/
b,
strong {
font-weight: bolder;
}
/**
1. Improve consistency of default fonts in all browsers. (https://github.com/sindresorhus/modern-normalize/issues/3)
2. Correct the odd 'em' font sizing in all browsers.
*/
code,
kbd,
samp,
pre {
font-family:
ui-monospace,
SFMono-Regular,
Consolas,
'Liberation Mono',
Menlo,
monospace; /* 1 */
font-size: 1em; /* 2 */
}
/**
Add the correct font size in all browsers.
*/
small {
font-size: 80%;
}
/**
Prevent 'sub' and 'sup' elements from affecting the line height in all browsers.
*/
sub,
sup {
font-size: 75%;
line-height: 0;
position: relative;
vertical-align: baseline;
}
sub {
bottom: -0.25em;
}
sup {
top: -0.5em;
}
/*
Tabular data
============
*/
/**
Correct table border color inheritance in Chrome and Safari. (https://issues.chromium.org/issues/40615503, https://bugs.webkit.org/show_bug.cgi?id=195016)
*/
table {
border-color: currentcolor;
}
/*
Forms
=====
*/
/**
1. Change the font styles in all browsers.
2. Remove the margin in Firefox and Safari.
*/
button,
input,
optgroup,
select,
textarea {
font-family: inherit; /* 1 */
font-size: 100%; /* 1 */
line-height: 1.15; /* 1 */
margin: 0; /* 2 */
}
/**
Correct the inability to style clickable types in iOS and Safari.
*/
button,
[type='button'],
[type='reset'],
[type='submit'] {
-webkit-appearance: button;
}
/**
Remove the padding so developers are not caught out when they zero out 'fieldset' elements in all browsers.
*/
legend {
padding: 0;
}
/**
Add the correct vertical alignment in Chrome and Firefox.
*/
progress {
vertical-align: baseline;
}
/**
Correct the cursor style of increment and decrement buttons in Safari.
*/
::-webkit-inner-spin-button,
::-webkit-outer-spin-button {
height: auto;
}
/**
1. Correct the odd appearance in Chrome and Safari.
2. Correct the outline style in Safari.
*/
[type='search'] {
-webkit-appearance: textfield; /* 1 */
outline-offset: -2px; /* 2 */
}
/**
Remove the inner padding in Chrome and Safari on macOS.
*/
::-webkit-search-decoration {
-webkit-appearance: none;
}
/**
1. Correct the inability to style clickable types in iOS and Safari.
2. Change font properties to 'inherit' in Safari.
*/
::-webkit-file-upload-button {
-webkit-appearance: button; /* 1 */
font: inherit; /* 2 */
}
/*
Interactive
===========
*/
/*
Add the correct display in Chrome and Safari.
*/
summary {
display: list-item;
}
}

69
css/tokens.css

@ -0,0 +1,69 @@
/**
* @file
* Design tokens for the druid theme.
*
* Custom properties live in the `tokens` cascade layer so any later layer
* (base, layout, components, utilities) can reference them without
* specificity concerns. To override a token in a narrower scope, re-declare
* it on a more specific selector inside a later layer e.g. inside
* `@layer components { .card { --surface-1: #fafafa; } }`.
*
*
* Adding tokens cherry-picked from Open Props
*
*
* Open Props (https://open-props.style) is a library of pre-made CSS custom
* properties. We do NOT import the full library instead, copy just the
* individual props we use, here, by hand.
*
* Why cherry-pick rather than @import the whole bundle:
* - Keeps payload small (the full library is ~20KB of props we'd never use).
* - No build step / dependency to manage.
* - You can see, in one file, every token the theme actually relies on.
*
* Workflow:
* 1. Browse https://open-props.style and find the token you want
* (e.g. `--shadow-3`, `--ease-elastic-1`, `--gray-7`).
* 2. Copy its declaration line both the name and the value into the
* appropriate section below. Preserve the Open Props name so you can
* look it up again later; rename only when you have a clear reason.
* 3. If you copy a token that depends on another OP token (e.g. a shadow
* that references `--shadow-color`), copy that one too.
* 4. Save. No build step, no `drush cr` needed for token *value* changes;
* a cache rebuild is only needed when adding/removing files or library
* entries.
*
* Reference for the source values:
* - Live docs: https://open-props.style
* - Source CSS: https://github.com/argyleink/open-props/tree/main/src
*/
@layer tokens {
:root {
/* Typography families registered in css/fonts.css; applied in css/base.css.
--font-sans body text (Open Sans, self-hosted variable font, weights 300800)
--font-serif headings (Adelle, self-hosted, Regular/Italic/Bold/BoldItalic)
System fallbacks ensure usable text before the @font-face files load. */
--font-sans: 'Open Sans', system-ui, sans-serif;
--font-serif: 'Adelle', Georgia, 'Times New Roman', serif;
/* Sizing scale — hand-picked, loosely follows Open Props `--size-N`. */
--size-1: 0.25rem;
--size-2: 0.5rem;
--size-3: 1rem;
--size-4: 1.5rem;
/* Border radius */
--radius-2: 8px;
/* Surfaces (backgrounds) */
--surface-1: #ffffff;
--surface-2: #f5f5f5;
/* Text colors */
--text-1: #111;
--text-2: #555;
/* Brand */
--brand: hsl(210 90% 50%);
}
}

27
css/utilities.css

@ -0,0 +1,27 @@
/**
* @file
* Single-purpose helper classes for the druid theme.
*
* The utilities layer is declared LAST in the cascade order, so any rule
* here wins over base / layout / components without specificity tricks.
* That's the whole point: a helper like .text-center exists to override
* whatever a component would otherwise apply, on demand.
*
* What belongs here:
* - Single-property, single-purpose helpers (.text-center, .mt-4, .flex)
* - Print-only / screen-only toggles (.print-only, .no-print)
*
* What does NOT belong here:
* - Multi-rule component styling use css/components/.
* - Token definitions use css/tokens.css.
* - .visually-hidden Drupal core's system module already ships this;
* don't redefine it or you'll fight the core selector for no reason.
*
* Gotcha: !important inverts cascade-layer order. An !important rule in
* an *earlier* layer (e.g. components) beats an !important rule here.
* Generally avoid !important inside @layer utilities the layer already
* wins on its own; reaching for !important means something else is wrong.
*/
@layer utilities {
}

22
druid.info.yml

@ -1,11 +1,31 @@
# druid theme — Drupal info metadata.
#
# The `libraries:` list below is attached on every page and is ordered to
# match the CSS cascade (layers → reset → tokens → base → layout →
# components → utilities). Don't reorder or remove entries from it:
# css/layers.css must parse before any `@layer NAME { … }` block, and the
# rest of the architecture assumes that load sequence. See CLAUDE.md for
# the full overview.
#
# The `libraries-extend:` block at the bottom maps core library names to
# theme libraries that should attach whenever the core library is in use.
# This is how conditional component CSS (dialog, dropbutton, file, etc.)
# loads only on pages that actually use those widgets.
name: druid name: druid
type: theme type: theme
'base theme': stable9 'base theme': stable9
version: 1.0.0 version: 1.0.0
libraries: libraries:
- druid/layers
- druid/reset
- druid/fonts
- druid/tokens
- druid/base - druid/base
- druid/layout
- druid/components
- druid/utilities
- druid/messages - druid/messages
- core/normalize
libraries-extend: libraries-extend:
user/drupal.user: user/drupal.user:
- druid/user - druid/user

63
druid.libraries.yml

@ -1,4 +1,67 @@
# druid theme — library definitions.
#
# The first seven libraries below (layers, reset, tokens, base, layout,
# components, utilities) each fill one slot in the cascade-layer
# architecture declared in css/layers.css. Their weights ascend in cascade
# order (-100 → -50) and their SMACSS categories are chosen so Drupal
# loads them in roughly the same order. See CLAUDE.md for the full
# architecture overview.
#
# Rules of thumb when editing this file:
# - Adding a new component CSS file: register it under the `components:`
# library with `weight: -10`, and wrap its rules in `@layer components`.
# - Don't reorder or remove the first seven libraries — load order must
# match cascade order so `css/layers.css` parses before any layered
# stylesheet.
# - Conditional libraries below (dialog, dropbutton, file, image-widget,
# indented, messages, node, progress, drupal.tablesort, item-list,
# search-results, user) are attached via `libraries-extend` in
# druid.info.yml when their corresponding core library is in use. Don't
# move them into the always-on list unless they should load everywhere.
layers:
version: VERSION
css:
base:
css/layers.css:
weight: -100
reset:
version: VERSION
css:
base:
css/lib/modern-normalize.css:
weight: -90
fonts:
version: VERSION
css:
base:
css/fonts.css:
weight: -85
tokens:
version: VERSION
css:
base:
css/tokens.css:
weight: -80
base: base:
version: VERSION
css:
base:
css/base.css:
weight: -70
layout:
version: VERSION
css:
layout:
css/layout.css:
weight: -60
utilities:
version: VERSION
css:
theme:
css/utilities.css:
weight: -50
components:
version: VERSION version: VERSION
css: css:
component: component:

BIN
fonts/adelle/Adelle_Bold.otf

Binary file not shown.

BIN
fonts/adelle/Adelle_Bold.woff2

Binary file not shown.

BIN
fonts/adelle/Adelle_BoldItalic.otf

Binary file not shown.

BIN
fonts/adelle/Adelle_BoldItalic.woff2

Binary file not shown.

BIN
fonts/adelle/Adelle_ExtraBold.otf

Binary file not shown.

BIN
fonts/adelle/Adelle_ExtraBold.woff2

Binary file not shown.

BIN
fonts/adelle/Adelle_ExtraBoldItalic.otf

Binary file not shown.

BIN
fonts/adelle/Adelle_ExtraBoldItalic.woff2

Binary file not shown.

BIN
fonts/adelle/Adelle_Heavy.otf

Binary file not shown.

BIN
fonts/adelle/Adelle_Heavy.woff2

Binary file not shown.

BIN
fonts/adelle/Adelle_HeavyItalic.otf

Binary file not shown.

BIN
fonts/adelle/Adelle_HeavyItalic.woff2

Binary file not shown.

BIN
fonts/adelle/Adelle_Italic.otf

Binary file not shown.

BIN
fonts/adelle/Adelle_Italic.woff2

Binary file not shown.

BIN
fonts/adelle/Adelle_LightItalic.otf

Binary file not shown.

BIN
fonts/adelle/Adelle_LightItalic.woff2

Binary file not shown.

BIN
fonts/adelle/Adelle_Reg.otf

Binary file not shown.

BIN
fonts/adelle/Adelle_Reg.woff2

Binary file not shown.

BIN
fonts/adelle/Adelle_SemiBoldItalic.otf

Binary file not shown.

BIN
fonts/adelle/Adelle_SemiBoldItalic.woff2

Binary file not shown.

BIN
fonts/adelle/Adelle_Semibold.otf

Binary file not shown.

BIN
fonts/adelle/Adelle_Semibold.woff2

Binary file not shown.

BIN
fonts/adelle/Adelle_light.otf

Binary file not shown.

BIN
fonts/adelle/Adelle_light.woff2

Binary file not shown.

BIN
fonts/open-sans/OpenSans-Italic-Latin.woff2

Binary file not shown.

BIN
fonts/open-sans/OpenSans-Italic-LatinExt.woff2

Binary file not shown.

BIN
fonts/open-sans/OpenSans-Latin.woff2

Binary file not shown.

BIN
fonts/open-sans/OpenSans-LatinExt.woff2

Binary file not shown.

210
todo.md

@ -0,0 +1,210 @@
# D11 theme set up
What I would recommend for your stack
Core stack
Reset / normalize
Use:
Modern Normalize
Very small and sensible.
Design tokens / vars
Use either:
your own variables
or selectively import from Open Props
Open Props
You do not need the whole library.
You can literally copy only the tokens you want.
Example:
:root {
--font-sans: system-ui, sans-serif;
--size-1: 0.25rem;
--size-2: 0.5rem;
--size-3: 1rem;
--size-4: 1.5rem;
--radius-2: 8px;
--surface-1: #ffffff;
--surface-2: #f5f5f5;
--text-1: #111;
--text-2: #555;
--brand: hsl(210 90% 50%);
}
That alone gets you surprisingly far.
Native CSS nesting is now good enough
You can now do:
.card {
padding: var(--size-4);
& h2 {
margin-block-start: 0;
}
& a {
color: var(--brand);
}
}
No Sass needed.
Browser support is now solid.
Cascade layers are a huge replacement for old Sass architecture
This is one of the biggest modern CSS improvements.
@layer reset, tokens, base, layout, components, utilities;
Then:
@layer base {
body {
font-family: var(--font-sans);
}
}
This gives you predictable override behavior without specificity nightmares.
Honestly better than many old Sass architectures.
Container queries are transformative for Drupal theming
Instead of:
@media (min-width: 900px)
you can do:
@container (min-width: 40rem)
This is fantastic for:
paragraphs
blocks
views
reusable Drupal components
because components respond to their container, not the viewport.
That maps beautifully to Drupal’s block-based structure.
Suggested file structure
Something like:
css/
reset.css
tokens.css
base.css
layout.css
components/
buttons.css
cards.css
forms.css
navigation.css
Then load in your .libraries.yml.
Very maintainable.
I would honestly skip Pico entirely then
Given what you just said, I think you may actually prefer:
normalize/reset
your own tokens
semantic HTML
native CSS features
tiny component CSS
instead of Pico.
Because Pico is opinionated.
You may eventually find yourself undoing its typography/layout assumptions.
Whereas your own token system:
stays tiny
teaches future-you exactly how the site works
avoids framework drift
keeps Drupal Twig very clean
Modern CSS features worth embracing immediately
These are absolutely worth using now:
clamp()
font-size: clamp(1rem, 2vw, 1.5rem);
logical properties
margin-block: 1rem;
padding-inline: 2rem;
Much cleaner than top/right/bottom/left.
:where()
Low-specificity selectors:
:where(article, section, aside) h2 {
margin-top: 0;
}
Excellent for maintainability.
color-mix()
background: color-mix(in srgb, var(--brand), white 90%);
Incredibly useful.
Tooling I would still keep
Even without preprocessing, I’d still consider:
Vite only for:
live reload
JS bundling
asset handling
NOT CSS transforms.
Or skip even that initially.
Modern Drupal theming can genuinely be:
CSS files
ES modules
libraries.yml
done.
And that’s refreshing compared to the old webpack/gulp era.
This approach fits Drupal especially well
Drupal’s strengths are:
semantic HTML
server rendering
reusable structured content
progressive enhancement
A lightweight native CSS architecture aligns with that extremely well.
You avoid importing frontend-app complexity into a CMS that doesn’t necessarily benefit from it.
──────────────────────────────────────────────────────────────────
Loading…
Cancel
Save