Initial commit

This commit is contained in:
jasa
2026-09-23 15:09:14 +02:00
commit e8a71dcb92
74 changed files with 2931 additions and 0 deletions
+4
View File
@@ -0,0 +1,4 @@
node_modules/
dist/
*.log
demo-dist/
+151
View File
@@ -0,0 +1,151 @@
# tlig_css
The TLIG stylesheet rebuilt on SASS tokens and Vite, as a replacement for
`Avada-Child-Theme/css/tlig_en.css` (7,082 lines) and the compiled Avada
`style.css` it fights with.
This first pass covers the common foundation only: fonts, colour, type scale,
links, buttons, form fields and the footer. Page-specific styling (messages,
bookstore, menus, the wood and scroll backgrounds) is not here yet.
## Commands
npm install
npm run dev # demo with live reload, at localhost:5173
npm run preview # demo as a static page, at localhost:4173
npm run build # the deliverable: dist/tlig.css + dist/fonts/
There is no way to open `src/index.html` from disk — it is an ES module that
imports `.scss`, so it needs one of the two servers above. `file://` gives a
blank page.
**`npm run dev`** compiles the SASS in memory and injects the CSS through
JavaScript. Fast, hot-reloading, and how you work. One caveat: because the CSS
arrives after first paint, anything with a `transition` animates up from its
unstyled state, so for a few milliseconds links are browser-blue and buttons are
nearly transparent. That is a dev-server artifact, not the stylesheet.
**`npm run preview`** builds the demo to `demo-dist/` and serves it. The page
there loads the compiled stylesheet through a real `<link rel="stylesheet">` in
`<head>`, so it paints once, exactly as a browser will on the real site. Use
this when judging how something actually looks, or when screenshotting.
**`npm run build`** emits only the deliverable — `dist/tlig.css` and the fonts,
no HTML and no demo markup. The demo is built from `src/index.html` and the
deliverable from `src/main.js`, so demo chrome cannot reach `dist`.
## The demo page
`npm run dev` serves `src/index.html`: the foundation rendered on the real
ground, inside the wood background and the papyrus scroll, with the real footer
component at the bottom. It shows the type scale, every link and button
variant, the dark-ground mechanism, and a colour grid generated at runtime from
the `--tlig-color-*` properties the stylesheet actually emits — so the grid
cannot drift from the code.
The toolbar has three controls:
- **Script** — switches the sample heading between Latin, Hungarian, Cyrillic
and Greek. No CSS changes when you do; the browser picks the font file by
codepoint. Also addressable as `?script=bg` for linking or screenshotting.
Greek is included deliberately: no papyrus cut exists for it, so it shows the
fallback face honestly.
- **Show boxes** — outlines each component so the structure is visible.
- A live viewport width and the active breakpoint.
The wood and the scroll are **demo chrome**, in `src/demo/demo.scss`, not in the
stylesheet: page layout is still on the to-do list below, and a demo is not the
place to quietly ship it. The demo is not a build input, so none of it can reach
`dist/tlig.css` — the build is checked for exactly that.
The frame is there anyway because a foundation only tells the truth on the
ground it has to work on. Brown ink at 86% looks arbitrary on white and
obviously right on papyrus.
## Layout
src/sass/
abstracts/ compile-time only; emits no CSS
_palette.scss every literal colour the live site uses
_tokens.scss semantic tokens: what a colour is *for*
_functions.scss token accessors
_mixins.scss media queries, type, patterns
base/
_root.scss emits every token as a CSS custom property
_fonts.scss @font-face
_reset.scss
_typography.scss
_links.scss
components/
_button.scss _field.scss _footer.scss
src/fonts/ papyrus-bold (4 scripts) and Lato, self-hosted
src/demo/ demo page chrome and behaviour — never built
src/index.html the demo page
## The three rules that replace the jigsaw
**One source of truth per value.** No rule writes a hex code. `_palette.scss`
holds the literals, `_tokens.scss` names them by role, `base/_root.scss` emits
them as `--tlig-*` custom properties, and every rule reads a token. `#482020`
appears 74 times in `tlig_en.css`; it appears once here.
**Context re-points tokens, it does not restate rules.** The `dark-ground`
mixin re-points nine custom properties; `.site-footer` and `.on-dark` both
include it and declare no colours of their own. Every link, heading and button
inside them is then correct — where the live CSS needs 24 id selectors to say
the same thing. The list of what "dark" means exists once, so the two cannot
drift: building the demo caught exactly that bug, an `.on-dark` that re-pointed
the links but not `--tlig-color-heading`, rendering a heading dark-brown on
dark-brown.
**Specificity goes one way.** Layer order in `main.scss` is tokens → base →
components, and nothing later needs to outrank anything earlier. Every
`!important` in the build — four of them — is in the single
`prefers-reduced-motion` block, where overriding author animation is the point.
There are none anywhere else. `tlig_en.css` has 518.
## What the tokens were taken from
Values are the ones the site renders today, not new choices:
| Token | Value | Source |
|---|---|---|
| `--tlig-color-page-ink` | `rgba(72,32,32,0.86)` | Avada `--body_typography-color` |
| `--tlig-color-link` | `#810008` | Avada `--link_color` |
| `--tlig-color-link-hover` | `#ca4a1f` | `tlig_en.css` global `a:hover` |
| `--tlig-color-heading` | `#482020` | Avada `--h1_typography-color` |
| `--tlig-color-heading-message` | `#7a0012` | `tlig_en.css` global `h2` |
| `--tlig-color-footer-ink` | `#fbefc9` | `tlig_en.css` `#footer-container` |
| `--tlig-color-button-bg` | `#8f0b06` | `tlig_en.css` `.btnSubmitNewsletter` |
| `--tlig-size-base` | `18px` | Avada `--body_typography-font-size` |
Two deliberate departures, both noted in the source:
- **h2** is encoded as Trebuchet / `#7a0012` / bold. Avada's own token says Lato
/ `#333333`, but `tlig_en.css` overrides it globally with four `!important`s,
so the second is what actually renders.
- **Spacing** is snapped to a 4px scale. The live CSS uses 6px, 23px, 35px and
46px in adjacent rules; those become the nearest step.
## Fonts
`papyrus-bold` is declared four times under one family name, split by
`unicode-range` across the Latin, Latin-Extended (Hungarian), Cyrillic and
Cyrillic-Extended (Bulgarian) files. The browser picks the file by codepoint, so
a Bulgarian heading needs no language class and no second stylesheet.
All font files are project assets. The live site points `@font-face` at absolute
`ww3.tlig.org` URLs, which is why a staging copy renders in the fallback face;
here Vite emits them into `dist/fonts/` and the CSS references them relatively,
so the bundle resolves at whatever path it is dropped.
Lato is self-hosted from the three files already in the child theme, replacing
the 33 `@font-face` blocks Avada generates from its Google Fonts cache.
## Not done yet
- Page-level layout: header, nav, the wood and papyrus backgrounds, `#main`
- Message typography (`.tbody`, `.vassula`, `.jc`, `.god`, the bible-reference
asides)
- Menus, bookstore, search, testimonies, downloads
- Enqueuing the built CSS from the theme, and retiring `tlig_en.css`
+1175
View File
File diff suppressed because it is too large Load Diff
+17
View File
@@ -0,0 +1,17 @@
{
"name": "tlig-css",
"private": true,
"version": "0.1.0",
"type": "module",
"description": "TLIG stylesheet, rebuilt on SASS tokens and Vite.",
"scripts": {
"dev": "vite",
"build": "vite build",
"build:demo": "vite build --mode demo",
"preview": "vite build --mode demo && vite preview --mode demo"
},
"devDependencies": {
"sass": "^1.105.0",
"vite": "^8.3.0"
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 178 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 72 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 98 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 126 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 152 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 150 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 154 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 160 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 135 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 91 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 101 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 283 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 57 KiB

+128
View File
@@ -0,0 +1,128 @@
// Demo page behaviour. Not a build input — see src/demo/demo.scss.
import '../sass/main.scss'
import './demo.scss'
// -- Colour swatches ---------------------------------------------------------
//
// Read from the emitted custom properties rather than from a hand-kept list, so
// the demo cannot drift from the stylesheet: delete a token and its swatch
// disappears on reload.
function emittedTokens (group) {
const names = new Set()
for (const sheet of document.styleSheets) {
let rules
try { rules = sheet.cssRules } catch { continue } // cross-origin
for (const rule of rules) {
if (rule.selectorText !== ':root') continue
for (const prop of rule.style) {
if (prop.startsWith(`--tlig-${group}-`)) names.add(prop)
}
}
}
return [...names]
}
function renderSwatches () {
const root = getComputedStyle(document.documentElement)
const html = emittedTokens('color').map((name) => {
const value = root.getPropertyValue(name).trim()
const short = name.replace('--tlig-color-', '')
return `<figure class="swatch">
<div class="swatch__chip"><div class="swatch__fill" style="background:${value}"></div></div>
<figcaption class="swatch__meta">
<div class="swatch__name">${short}</div>
<div class="swatch__value">${value}</div>
</figcaption>
</figure>`
}).join('')
document.querySelector('#swatches').innerHTML = html
document.querySelector('#swatch-count').textContent = emittedTokens('color').length
}
// -- Script switcher ---------------------------------------------------------
//
// The point of this control: all four papyrus files share one family name and
// are separated by unicode-range, so switching scripts changes no CSS at all.
// The browser picks the font file by codepoint.
const SAMPLES = {
en: {
label: 'English',
heading: 'True Life in God',
line: 'Peace be with you — I am the Alpha and the Omega.',
note: 'Latin — PapyrusLETBold, the original file.',
},
hu: {
// The pangram, not the site's own title: "Igaz Élet Istenben" contains no
// codepoint above U+00FF, so it would be served by the Latin file and
// would demonstrate nothing. ő (U+0151) and ű (U+0171) are the test.
label: 'Magyar',
heading: 'Árvíztűrő tükörfúrógép',
line: 'Igaz Élet Istenben — ő and ű are what the Latin-Extended cut is for.',
note: 'Latin Extended-A — the hu file. The original PapyrusLETBold has no ő or ű.',
},
bg: {
label: 'Български',
heading: 'Истински живот в Бога',
line: 'Мир на вас — Аз съм Алфа и Омега.',
note: 'Cyrillic — the cyrillic and bulgarian cuts.',
},
ru: {
label: 'Русский',
heading: 'Истинная жизнь в Боге',
line: 'Мир вам — Я есмь Альфа и Омега.',
note: 'Cyrillic — same file as Bulgarian, no extra rule.',
},
el: {
label: 'Ελληνικά',
heading: 'Αληθινή εν Θεώ Ζωή',
line: 'Ειρήνη υμίν — Εγώ ειμι το Άλφα και το Ωμέγα.',
note: 'No papyrus cut exists for Greek — this is the fallback face, shown honestly.',
},
}
function setScript (code) {
const sample = SAMPLES[code]
if (!sample) return
document.querySelector('#script-heading').textContent = sample.heading
document.querySelector('#script-line').textContent = sample.line
document.querySelector('#script-note').textContent = sample.note
document.querySelector('#script-heading').lang = code
for (const button of document.querySelectorAll('[data-script]')) {
button.setAttribute('aria-pressed', String(button.dataset.script === code))
}
}
// -- Breakpoint readout ------------------------------------------------------
function trackViewport () {
const output = document.querySelector('#viewport')
const update = () => {
const width = window.innerWidth
const name = width >= 1200 ? 'wide' : width >= 800 ? 'tablet' : 'phone'
output.textContent = `${width}px · ${name}`
}
window.addEventListener('resize', update, { passive: true })
update()
}
// -- Wire up -----------------------------------------------------------------
renderSwatches()
trackViewport()
// `?script=bg` opens straight onto a given script, so a particular cut can be
// linked to or screenshotted without clicking.
const requested = new URLSearchParams(location.search).get('script')
setScript(requested in SAMPLES ? requested : 'en')
for (const button of document.querySelectorAll('[data-script]')) {
button.addEventListener('click', () => setScript(button.dataset.script))
}
const boxes = document.querySelector('#toggle-boxes')
boxes.addEventListener('click', () => {
const on = document.body.classList.toggle('show-boxes')
boxes.setAttribute('aria-pressed', String(on))
})
+256
View File
@@ -0,0 +1,256 @@
// -----------------------------------------------------------------------------
// Demo chrome — NOT part of the shipped stylesheet.
//
// Everything here exists to frame the foundation in something that looks like
// the real site (wood ground, papyrus scroll) and to give the page its toolbar
// and swatch grid. It is reached only from src/index.html, which is not a build
// input, so none of it can reach dist/tlig.css.
//
// The scroll frame is deliberately here and not in components/: page layout is
// still on the "not done" list, and a demo is not the place to quietly ship it.
// -----------------------------------------------------------------------------
@use '../sass/abstracts' as *;
$img: '/wp-content/includes/images';
// -- The page ground ---------------------------------------------------------
html {
background: url('#{$img}/background_planche_1920.jpg') top center / contain fixed;
}
body {
min-height: 100vh;
}
// The scroll: a head image, a vertically repeating middle, a foot image.
.scroll {
max-width: 1200px;
margin-inline: auto;
}
.scroll__head,
.scroll__foot {
background-repeat: no-repeat;
background-size: 100% 100%;
}
.scroll__head {
height: 140px;
background-image: url('#{$img}/page/background-message-entete_cnd.png.webp');
background-position: bottom;
}
.scroll__body {
margin-block-start: -1px;
padding: 0 clamp(#{space-raw('md')}, 5vw, 5rem) space('xl');
background: url('#{$img}/page/background-message-milieux1200x190-4_cnd.png.webp') repeat-y top / 100% auto;
}
.scroll__foot {
// The top 18px of this image ramp from alpha 15 up to its 242 plateau — it
// fades in. Butted against the body, that ramp composites over bare wood and
// reads as a hard line at the body's bottom edge. The overlap below is the
// measured ramp height plus a little, so the whole fade lands on papyrus.
//
// `top` cannot do this: the element is static, and only a positioned box
// moves. `position`/`z-index` pin the paint order the effect depends on —
// tree order already gives it, but silently.
position: relative;
z-index: 1;
margin-block-start: -20px;
height: 114px;
background-image: url('#{$img}/page/background-message-footer-1200x114_cnd.png.webp');
background-position: top;
}
// Below the tablet breakpoint the site swaps in narrower scroll images, which
// are not mirrored into this project. Rather than stretch the wide ones, the
// demo falls back to a plain parchment panel.
@include down('tablet') {
.scroll__head,
.scroll__foot { display: none; }
.scroll__body {
margin: space('md');
padding: space('lg');
border-radius: radius('md');
background: rgba(247, 240, 224, 0.92);
}
}
// -- Section scaffolding -----------------------------------------------------
.demo-section {
padding-block: space('xl');
& + & {
border-block-start: 1px solid rgba(145, 116, 85, 0.35);
}
}
.demo-label {
display: flex;
align-items: baseline;
gap: space('sm');
margin-block-end: space('lg');
font-family: font('body');
font-size: 11px;
font-weight: 700;
letter-spacing: 0.14em;
text-transform: uppercase;
color: $brown-600;
&::after {
content: '';
flex: 1;
height: 1px;
background: currentColor;
opacity: 0.35;
}
}
.demo-note {
margin-block-start: space('sm');
font-family: font('body');
font-size: 13px;
font-style: italic;
color: $brown-600;
}
// A row of specimens that should sit side by side and wrap sanely.
.demo-row {
display: flex;
flex-wrap: wrap;
gap: space('md');
align-items: center;
}
// -- Swatches ----------------------------------------------------------------
.swatch-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));
gap: space('sm');
}
.swatch {
overflow: hidden;
border: 1px solid rgba(72, 32, 32, 0.15);
border-radius: radius('sm');
background: $white;
font-family: font('body');
font-size: 11px;
line-height: 1.5;
// Checkerboard, so an alpha token reads as translucent rather than as a
// slightly different flat colour.
&__chip {
height: 56px;
background-image:
linear-gradient(45deg, #ddd 25%, transparent 25%, transparent 75%, #ddd 75%),
linear-gradient(45deg, #ddd 25%, transparent 25%, transparent 75%, #ddd 75%);
background-size: 12px 12px;
background-position: 0 0, 6px 6px;
}
&__fill { height: 100%; }
&__meta { padding: space('2xs') space('xs'); }
&__name { font-weight: 700; color: $brown-900; }
&__value { color: $grey-500; font-variant-numeric: tabular-nums; }
}
// -- Type specimen table -----------------------------------------------------
.specimen {
display: grid;
grid-template-columns: 8rem 1fr;
gap: space('sm') space('lg');
align-items: baseline;
@include down('phone') {
grid-template-columns: 1fr;
gap: space('2xs');
}
}
.specimen__key {
font-family: font('body');
font-size: 11px;
letter-spacing: 0.08em;
text-transform: uppercase;
color: $brown-600;
}
// -- Dark-ground demo --------------------------------------------------------
.demo-dark {
padding: space('lg');
border-radius: radius('md');
background: $brown-900;
}
// -- Toolbar -----------------------------------------------------------------
.toolbar {
position: sticky;
top: 0;
z-index: 10;
display: flex;
flex-wrap: wrap;
gap: space('md');
align-items: center;
padding: space('xs') space('md');
background: rgba(30, 14, 14, 0.94);
color: #f0e6d2;
font-family: font('body');
font-size: 12px;
backdrop-filter: blur(6px);
&__brand {
font-weight: 700;
letter-spacing: 0.1em;
text-transform: uppercase;
}
&__group {
display: flex;
gap: space('2xs');
align-items: center;
}
&__spacer { flex: 1; }
button {
padding: 3px 10px;
border: 1px solid rgba(240, 230, 210, 0.3);
border-radius: radius('pill');
background: transparent;
color: inherit;
font-size: 12px;
&[aria-pressed='true'] {
background: #f0e6d2;
color: #1e0e0e;
border-color: #f0e6d2;
}
}
code {
font-variant-numeric: tabular-nums;
opacity: 0.8;
}
}
// "Show the boxes" — outlines every component so the structure is visible.
.show-boxes {
.site-footer__col,
.demo-row > *,
.field-group,
.scroll__body > * {
outline: 1px dashed rgba(202, 74, 31, 0.6);
outline-offset: 2px;
}
}
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+228
View File
@@ -0,0 +1,228 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>TLIG — stylesheet demo</title>
<script type="module" src="/demo/demo.js"></script>
</head>
<body>
<div class="toolbar">
<span class="toolbar__brand">tlig.css</span>
<span class="toolbar__group">
<span>Script:</span>
<button type="button" data-script="en" aria-pressed="true">English</button>
<button type="button" data-script="hu" aria-pressed="false">Magyar</button>
<button type="button" data-script="bg" aria-pressed="false">Български</button>
<button type="button" data-script="ru" aria-pressed="false">Русский</button>
<button type="button" data-script="el" aria-pressed="false">Ελληνικά</button>
</span>
<span class="toolbar__spacer"></span>
<button type="button" id="toggle-boxes" aria-pressed="false">Show boxes</button>
<code id="viewport"></code>
</div>
<div class="scroll">
<div class="scroll__head"></div>
<div class="scroll__body">
<!-- ------------------------------------------------------------------ -->
<section class="demo-section">
<h1>A stylesheet without the jigsaw</h1>
<div class="prose">
<p>Everything below is rendered by <code>src/sass/main.scss</code> — 10 kB
compiled, and every <code>!important</code> in it (four) sits in the
one <code>prefers-reduced-motion</code> block, where overriding author
animation is the point. <code>tlig_en.css</code> has 518. The wood ground and the papyrus scroll around this
text are <em>demo chrome</em>, not part of the stylesheet: page layout
is still on the to-do list, and a demo is not the place to quietly
ship it.</p>
<p>The frame is here anyway because a foundation only tells you the
truth when you see it on the ground it has to work on. Brown ink at
86% opacity looks arbitrary on white and obviously right on papyrus.</p>
</div>
</section>
<!-- ------------------------------------------------------------------ -->
<section class="demo-section">
<p class="demo-label">One family name, four files</p>
<h1 id="script-heading">True Life in God</h1>
<p class="prose" id="script-line"></p>
<p class="demo-note" id="script-note"></p>
<p class="demo-note">Use the toolbar to switch script. No CSS changes, no
language class, no second stylesheet — the four papyrus cuts share the
family name <code>papyrus-bold</code> and are separated by
<code>unicode-range</code>, so the browser picks the file by codepoint.</p>
</section>
<!-- ------------------------------------------------------------------ -->
<section class="demo-section">
<p class="demo-label">Type scale</p>
<div class="specimen">
<span class="specimen__key">h1 · 34px</span>
<h1>Papyrus-bold, tight leading</h1>
<span class="specimen__key">h2 · 22px</span>
<h2>Trebuchet, message red, bold</h2>
<span class="specimen__key">h3 · 20px</span>
<h3>Lato bold, heading ink</h3>
<span class="specimen__key">h4 · 25px</span>
<h4>Papyrus-bold, the section mark</h4>
<span class="specimen__key">h5 · 14px</span>
<h5>Lato bold, small heading</h5>
<span class="specimen__key">h6 · 12px</span>
<h6>Lato bold, uppercase</h6>
<span class="specimen__key">body · 18px</span>
<p>Lato at 18px on a 1.5 line height, in
<code>--tlig-color-page-ink</code>: a brown at 86% rather than black,
so it settles onto the papyrus instead of sitting on top of it. Here
is <strong>bold text</strong>, <em>italic text</em>, and
<small>small print in the muted token</small>.</p>
<span class="specimen__key">blockquote</span>
<blockquote>The translucent panel — <code>--tlig-color-page-panel</code>
at 30% white — with the message-red rule and the asymmetric corner the
site has always had.</blockquote>
</div>
<p class="demo-note">h2 is encoded as Trebuchet / #7a0012 / bold. Avada's
own token says Lato / #333333, but <code>tlig_en.css</code> overrides it
globally with four <code>!important</code>s — so the override is what
actually renders, and the override is what got written down.</p>
</section>
<!-- ------------------------------------------------------------------ -->
<section class="demo-section">
<p class="demo-label">Links</p>
<div class="prose">
<p>A <a href="#">default link</a> carries no underline — that is for
navigation and cards. Inside <code>.prose</code>, <a href="#">a link
underlines itself</a> without needing a class. The explicit
<a href="#" class="link-underline">.link-underline</a> variant is kept
for templates that ask for it, and <a href="#" class="link-plain">.link-plain</a>
inherits its colour, for a link wrapping a whole card.</p>
</div>
<p class="demo-note">The live rule is
<code>a:hover { color: #ca4a1f !important }</code> — one global
<code>!important</code> that every later link style then has to fight.
That is where most of the 518 in <code>tlig_en.css</code> start.</p>
</section>
<!-- ------------------------------------------------------------------ -->
<section class="demo-section">
<p class="demo-label">Buttons and fields</p>
<div class="demo-row">
<a href="#" class="button">View the magazine</a>
<a href="#" class="button button--donate">Donate</a>
<button class="button" type="button" disabled>Disabled</button>
</div>
<form class="field-group" style="margin-top:1.5rem" onsubmit="return false">
<input class="field field--attached" type="email" placeholder="Your email address" aria-label="Email address">
<button class="button button--attached" type="submit">Subscribe</button>
</form>
<p class="demo-note">Four ids and classes in the live CSS —
<code>#footer-viewMagazineSpan</code>, <code>.btnSubmitNewsletter</code>,
<code>.btnSubmitDailyMessage</code>, <code>.footer-donateSpan</code>
repeating the same six declarations with different corner radii. One
class with modifiers replaces them.</p>
</section>
<!-- ------------------------------------------------------------------ -->
<section class="demo-section">
<p class="demo-label">Context re-points tokens</p>
<div class="demo-dark on-dark">
<h3>The same markup on a dark ground</h3>
<p>This block adds one class. It declares no colours of its own: the
<code>dark-ground</code> mixin re-points nine tokens, and this
heading, this body copy, this <a href="#">link</a>, its hover and the
button below are all correct as a result. No dark-specific rule for
any of them exists in the stylesheet.</p>
<p style="margin-top:1rem"><a href="#" class="button">A button, unchanged</a></p>
</div>
<p class="demo-note">This is the mechanism that replaces the footer's 24
id selectors: a region declares what its tokens mean, and every
component inside it is already right. The footer below includes the same
mixin — the list of what "dark" means exists once, so the two cannot
drift. An earlier draft of this page caught the bug that proves the
point: <code>.on-dark</code> had re-pointed the links but not
<code>--tlig-color-heading</code>, and the heading above rendered
dark-brown on dark-brown.</p>
</section>
<!-- ------------------------------------------------------------------ -->
<section class="demo-section">
<p class="demo-label">Colour tokens (<span id="swatch-count">0</span>)</p>
<div class="swatch-grid" id="swatches"></div>
<p class="demo-note">Generated at runtime from the <code>--tlig-color-*</code>
properties the stylesheet actually emits, not from a hand-kept list — so
this grid cannot drift. Delete a token and its swatch disappears.</p>
</section>
</div>
<div class="scroll__foot"></div>
</div>
<!-- The real footer component, not a mock ------------------------------- -->
<footer class="site-footer">
<div class="site-footer__inner">
<div class="site-footer__row">
<div class="site-footer__col site-footer__col--wide">
<p class="site-footer__title">The True Life in God magazine</p>
<p class="site-footer__text">Read the latest issue online, or order a
printed copy for your prayer group.</p>
<p style="margin-top:1rem"><a href="#" class="button">View the magazine</a></p>
</div>
<div class="site-footer__col">
<p class="site-footer__title">Newsletter</p>
<form class="field-group" onsubmit="return false">
<input class="field field--attached" type="email" placeholder="Your email address" aria-label="Email address">
<button class="button button--attached" type="submit">Subscribe</button>
</form>
<p class="site-footer__title" style="margin-top:2rem">Daily message</p>
<p><a href="#" class="button">Receive it by email</a></p>
</div>
</div>
<div class="site-footer__row">
<div class="site-footer__col">
<p class="site-footer__title">What is…</p>
<ul class="site-footer__menu">
<li><a href="#">What is True Life in God?</a></li>
<li><a href="#">Who is Vassula Rydén?</a></li>
<li><a href="#">The messages</a></li>
</ul>
</div>
<div class="site-footer__col">
<p class="site-footer__title">Support the mission</p>
<p><a href="#" class="button button--donate">Donate</a></p>
</div>
<div class="site-footer__col">
<p class="site-footer__title">Follow</p>
<ul class="site-footer__social">
<li><a href="#">Facebook</a></li>
<li><a href="#">YouTube</a></li>
<li><a href="#">Instagram</a></li>
</ul>
</div>
</div>
</div>
</footer>
</body>
</html>
+3
View File
@@ -0,0 +1,3 @@
// Entry point. Its only job today is to pull the stylesheet into the build;
// behaviour belongs in src/js/ and gets imported here as it is written.
import './sass/main.scss'
+37
View File
@@ -0,0 +1,37 @@
// -----------------------------------------------------------------------------
// Token accessors.
//
// These return `var(--tlig-…)` rather than the literal, so a token stays
// overridable at runtime. Use `*-raw()` only where a custom property cannot go
// — inside a media query condition, or in a `@font-face` block.
// -----------------------------------------------------------------------------
@use 'sass:map';
@use 'tokens' as *;
@function _var($group, $key, $map) {
@if not map.has-key($map, $key) {
@error 'Unknown #{$group} token "#{$key}". Known: #{map.keys($map)}.';
}
@return var(--#{$prefix}-#{$group}-#{$key});
}
@function color($key) { @return _var('color', $key, $colors); }
@function font($key) { @return _var('font', $key, $fonts); }
@function size($key) { @return _var('size', $key, $font-sizes); }
@function leading($key){ @return _var('leading', $key, $line-heights); }
@function weight($key) { @return _var('weight', $key, $font-weights); }
@function space($key) { @return _var('space', $key, $space); }
@function radius($key) { @return _var('radius', $key, $radii); }
// Compile-time literals, for the few places a custom property is not allowed.
@function color-raw($key) { @return map.get($colors, $key); }
@function font-raw($key) { @return map.get($fonts, $key); }
@function space-raw($key) { @return map.get($space, $key); }
@function breakpoint($key) {
@if not map.has-key($breakpoints, $key) {
@error 'Unknown breakpoint "#{$key}". Known: #{map.keys($breakpoints)}.';
}
@return map.get($breakpoints, $key);
}
+5
View File
@@ -0,0 +1,5 @@
// Single import surface for the abstract layer: `@use '../abstracts' as *;`
@forward 'palette';
@forward 'tokens';
@forward 'functions';
@forward 'mixins';
+89
View File
@@ -0,0 +1,89 @@
@use 'sass:map';
@use 'tokens' as *;
@use 'functions' as *;
// -- Media queries -----------------------------------------------------------
//
// Mobile-first: `up()` opens a floor, `down()` a ceiling, and `down()` subtracts
// 0.02px so the two never both match on the same viewport. The live CSS has
// exactly that overlap bug at 800px.
@mixin up($name) {
@media (min-width: #{breakpoint($name)}) { @content; }
}
@mixin down($name) {
@media (max-width: #{breakpoint($name) - 0.02px}) { @content; }
}
@mixin between($from, $to) {
@media (min-width: #{breakpoint($from)}) and (max-width: #{breakpoint($to) - 0.02px}) {
@content;
}
}
// -- Type --------------------------------------------------------------------
@mixin type($family: 'body', $size: 'base', $leading: 'normal', $weight: 'normal') {
font-family: font($family);
font-size: size($size);
line-height: leading($leading);
font-weight: weight($weight);
}
// Headings on this site are the display face at a fixed size and tight leading.
@mixin heading($size: '2xl') {
font-family: font('display');
font-size: size($size);
line-height: leading('tight');
font-weight: weight('normal');
color: color('heading');
}
// -- Patterns ----------------------------------------------------------------
// The translucent panel that carries text over the papyrus background.
@mixin panel($pad: 'lg', $round: 'md') {
padding: space($pad);
background-color: color('page-panel');
border-radius: radius($round);
}
// Visible only to screen readers.
@mixin visually-hidden {
position: absolute;
width: 1px;
height: 1px;
margin: -1px;
padding: 0;
overflow: hidden;
clip-path: inset(50%);
white-space: nowrap;
border: 0;
}
// A focus ring that survives on both the light page and the dark footer.
@mixin focus-ring($offset: 2px) {
outline: 2px solid currentColor;
outline-offset: $offset;
}
// Re-point the token set for a dark ground.
//
// This is the whole contract in one place: any region that includes it gets
// correct body copy, headings, links and hovers with no further declarations.
// It must cover *every* token that encodes a light-ground assumption — an
// incomplete list is how a heading ends up dark-brown on dark-brown.
@mixin dark-ground {
--#{$prefix}-color-page-ink: #{map.get($colors, 'footer-ink')};
--#{$prefix}-color-page-ink-strong: #{map.get($colors, 'footer-ink')};
--#{$prefix}-color-page-ink-muted: #{map.get($colors, 'footer-rule')};
--#{$prefix}-color-heading: #{map.get($colors, 'footer-ink')};
--#{$prefix}-color-heading-message: #{map.get($colors, 'footer-link-hover')};
--#{$prefix}-color-link: #{map.get($colors, 'footer-link')};
--#{$prefix}-color-link-hover: #{map.get($colors, 'footer-link-hover')};
--#{$prefix}-color-link-underlined: #{map.get($colors, 'footer-link')};
--#{$prefix}-color-page-rule: #{map.get($colors, 'footer-rule')};
color: color('page-ink');
}
+66
View File
@@ -0,0 +1,66 @@
// -----------------------------------------------------------------------------
// Raw palette — every literal colour the live site actually uses.
//
// Nothing outside this file may write a hex value. Rules consume the *semantic*
// tokens in `_tokens.scss`, never these names directly: a raw name says what a
// colour looks like, a semantic name says what it is for, and only the second
// survives a redesign.
//
// Source: `Avada-Child-Theme/style.css` (`:root` fusion variables) and
// `Avada-Child-Theme/css/tlig_en.css`, September 2026.
// -----------------------------------------------------------------------------
// Browns — the site's "ink"; the wood background and papyrus scroll sit under
// everything, so the darkest text tone is a brown, never black.
$brown-900: #482020; // 74 uses in tlig_en.css — the most common colour
$brown-800: #511a06; // message body text
$brown-600: #917455; // wood / border tone
$brown-300: #c8b29a; // newsletter input field
// Reds — link, heading and call-to-action family.
$red-900: #7a0012; // message headings (h2)
$red-800: #810008; // Avada --link_color
$red-780: #7e050c; // .linkUnderline
$red-700: #8f0b06; // primary button fill
$red-650: #770000; // mobile header / menu
$red-600: #951111; // blockquote rule
$red-500: #961a07; // alert
$red-400: #ca4a1f; // link hover (global `a:hover` in tlig_en.css)
// Golds — footer, accents, hover states on dark ground.
$gold-700: #d8ae0b; // donate button
$gold-600: #ffc600; // sliding-bar link hover
$gold-500: #f9d368; // footer dropdown rules
$gold-400: #ffe683; // hover on dark
$gold-300: #ffe7bf; // mobile menu hover
$gold-200: #fff8c4; // mobile menu text
$gold-100: #fbefc9; // footer text
// Neutrals.
$white: #ffffff;
$grey-050: #fbfbfb;
$grey-100: #f0f0f0;
$grey-200: #eaeaea;
$grey-300: #d6d6d6;
$grey-400: #bbbbbb;
$grey-500: #989898;
$grey-700: #333333;
$black: #0a0a0a;
// Semantic accents used by notices and status chips.
$green-600: #53961e;
$green-400: #96d865;
$blue-700: #244c80;
$sand-100: #fcf8e3;
// Alpha tints. Kept as literals rather than computed, because these exact
// values are what ships today and a rounding difference would be a visible
// change dressed up as a refactor.
$ink-86a: rgba(72, 32, 32, 0.86); // body text
$ink-50a: rgba(72, 32, 32, 0.5);
$wood-50a: rgba(145, 116, 85, 0.5);
$wood-20a: rgba(145, 116, 85, 0.2);
$white-80a: rgba(255, 255, 255, 0.8);
$white-30a: rgba(255, 255, 255, 0.3); // the translucent "paper" panels
$shadow-35a: rgba(0, 0, 0, 0.35);
$shadow-15a: rgba(0, 0, 0, 0.15);
+139
View File
@@ -0,0 +1,139 @@
// -----------------------------------------------------------------------------
// Semantic tokens.
//
// One map per concern. Each map is emitted as CSS custom properties by
// `base/_root.scss`, so a token can be re-pointed per language or per section
// at runtime without recompiling — which is how the per-language papyrus and
// the dark footer get handled without a second stylesheet.
// -----------------------------------------------------------------------------
@use 'palette' as *;
$prefix: 'tlig';
// -- Colour ------------------------------------------------------------------
$colors: (
// Page
'page-ink': $ink-86a, // body copy
'page-ink-strong': $brown-900, // headings, emphasis
'page-ink-muted': $grey-500,
'page-panel': $white-30a, // translucent card over the scroll
'page-rule': $grey-300,
// Links
'link': $red-800,
'link-hover': $red-400,
'link-underlined': $red-780, // the `.link-underline` variant
// Headings
'heading': $brown-900,
'heading-message': $red-900, // h2 inside a message
// Message typography (the scroll pages)
'message-body': $brown-800,
'message-aside': $brown-600,
// Footer — the one region on a dark ground
'footer-bg': $brown-900,
'footer-ink': $gold-100,
'footer-link': $white,
'footer-link-hover': $gold-400,
'footer-rule': $gold-500,
'footer-panel': $black,
// Buttons
'button-bg': $red-700,
'button-ink': $white,
'button-ink-hover': $gold-400,
'button-donate-bg': $gold-700,
'button-donate-ink': $white,
// Forms
'field-bg': $brown-300,
'field-ink': $white,
'field-placeholder': rgba(255, 255, 255, 0.8),
// Status
'ok': $green-600,
'ok-soft': $green-400,
'alert': $red-500,
'info': $blue-700,
'notice-bg': $sand-100,
// Chrome
'shadow': $shadow-35a,
'shadow-soft': $shadow-15a,
);
// -- Type --------------------------------------------------------------------
// `papyrus-bold` is the display face; the per-script files are bound to the
// same family name via unicode-range in `base/_fonts.scss`, so a Bulgarian
// heading needs no separate rule.
$fonts: (
'display': (papyrus-bold, 'Papyrus', fantasy),
'body': (Lato, 'Trebuchet MS', Helvetica, sans-serif),
'message': ('Trebuchet MS', Verdana, sans-serif),
'nav': (Oswald, 'Lato', sans-serif),
);
$font-sizes: (
'xs': 12px,
'sm': 14px,
'base': 18px, // Avada --body_typography-font-size
'md': 20px,
'lg': 22px,
'xl': 25px,
'2xl': 34px, // h1
);
$line-heights: (
'tight': 1.2, // headings
'snug': 1.35,
'normal': 1.5, // body
'message': 1.33, // 24px on 18px
);
$font-weights: (
'normal': 400,
'medium': 600,
'bold': 700,
);
// -- Space -------------------------------------------------------------------
// A 4px-based scale. The live CSS uses arbitrary values (6px, 23px, 46px…);
// those are rounded to the nearest step here. Every such rounding is invisible
// at a glance and is the point of the exercise — one rhythm, not forty.
$space: (
'3xs': 2px,
'2xs': 4px,
'xs': 8px,
'sm': 12px,
'md': 16px,
'lg': 24px,
'xl': 32px,
'2xl': 48px,
'3xl': 64px,
);
$radii: (
'sm': 5px,
'md': 7px,
'lg': 20px,
'pill': 999px,
);
// -- Layout ------------------------------------------------------------------
$site-width: 1200px; // Avada --site_width
// Named breakpoints. The live CSS switches at 480 and 799/800 with a one-pixel
// gap between `max-width: 800px` and `min-width: 800px` rules; that gap is
// closed here by driving everything from these two numbers.
$breakpoints: (
'phone': 480px,
'tablet': 800px,
'wide': 1200px,
);
+85
View File
@@ -0,0 +1,85 @@
// -----------------------------------------------------------------------------
// @font-face.
//
// The live site declares papyrus-bold once, in the compiled Avada stylesheet,
// pointing at absolute ww3.tlig.org URLs — which is why a staging copy renders
// with the fallback face. Here the files are project assets: Vite fingerprints
// them and rewrites the URL, so the bundle is self-contained wherever it lands.
//
// All four papyrus files share one family name and are separated by
// unicode-range. A Bulgarian or Hungarian heading therefore needs no language
// class and no second stylesheet — the browser picks the file by codepoint.
// -----------------------------------------------------------------------------
// Latin — the original PapyrusLETBold.
@font-face {
font-family: papyrus-bold;
src: url('/fonts/papyrus/papyrus-bold-latin.woff2') format('woff2'),
url('/fonts/papyrus/papyrus-bold-latin.woff') format('woff');
font-weight: 400;
font-style: normal;
font-display: swap;
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6,
U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122,
U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
// Latin Extended-A — Hungarian needs ő and ű, which the original file lacks.
@font-face {
font-family: papyrus-bold;
src: url('/fonts/papyrus/papyrus-bold-hu.woff2') format('woff2');
font-weight: 400;
font-style: normal;
font-display: swap;
unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2C60-2C7F, U+A720-A7FF;
}
// Cyrillic.
@font-face {
font-family: papyrus-bold;
src: url('/fonts/papyrus/papyrus-bold-cyrillic.woff2') format('woff2'),
url('/fonts/papyrus/papyrus-bold-cyrillic.woff') format('woff');
font-weight: 400;
font-style: normal;
font-display: swap;
unicode-range: U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}
// Cyrillic Extended — the Bulgarian cut covers the forms the base file misses.
@font-face {
font-family: papyrus-bold;
src: url('/fonts/papyrus/papyrus-bold-bulgarian.woff') format('woff');
font-weight: 400;
font-style: normal;
font-display: swap;
unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F;
}
// Lato — self-hosted. The live site pulls this from Google via Avada's
// fusion-gfonts cache, 33 @font-face blocks for what is three files.
@font-face {
font-family: Lato;
src: url('/fonts/lato/Lato-Medium.woff2') format('woff2'),
url('/fonts/lato/Lato-Medium.woff') format('woff');
font-weight: 400;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: Lato;
src: url('/fonts/lato/Lato-MediumItalic.woff2') format('woff2'),
url('/fonts/lato/Lato-MediumItalic.woff') format('woff');
font-weight: 400;
font-style: italic;
font-display: swap;
}
@font-face {
font-family: Lato;
src: url('/fonts/lato/Lato-Bold.woff2') format('woff2'),
url('/fonts/lato/Lato-Bold.woff') format('woff');
font-weight: 700;
font-style: normal;
font-display: swap;
}
+5
View File
@@ -0,0 +1,5 @@
@forward 'root';
@forward 'fonts';
@forward 'reset';
@forward 'typography';
@forward 'links';
+51
View File
@@ -0,0 +1,51 @@
// -----------------------------------------------------------------------------
// Links.
//
// The live rule is `a:hover { color: #ca4a1f !important; }` — one global
// `!important` that every later link style then has to fight, which is where
// most of the `!important` inflation in `tlig_en.css` starts. Here the hover
// colour is inherited from a token, so a context that needs a different hover
// (the footer, a button) simply re-points `--tlig-color-link-hover`.
// -----------------------------------------------------------------------------
@use '../abstracts' as *;
a {
color: color('link');
text-decoration: none;
transition: color 120ms ease-in-out;
&:hover,
&:focus-visible {
color: color('link-hover');
}
}
// Body copy inside prose gets an underline; navigation and cards do not.
.prose a,
.link-underline {
color: color('link-underlined');
text-decoration: underline;
text-underline-offset: 0.15em;
&:hover,
&:focus-visible {
color: color('link-hover');
}
}
// A link wrapping a card or an image should not tint its contents.
.link-plain {
color: inherit;
&:hover,
&:focus-visible {
color: inherit;
}
}
// Any region that re-points the token set gets correct links for free — this
// is what the footer does, via the same mixin.
.on-dark {
@include dark-ground;
}
+69
View File
@@ -0,0 +1,69 @@
// -----------------------------------------------------------------------------
// Reset. Deliberately small — it normalises box model and defaults, and stops.
// -----------------------------------------------------------------------------
@use '../abstracts' as *;
*,
*::before,
*::after {
box-sizing: border-box;
}
html {
-webkit-text-size-adjust: 100%;
text-size-adjust: 100%;
}
body {
margin: 0;
}
// Headings and paragraphs carry their spacing from the type scale, not from
// whatever the user agent thinks.
h1, h2, h3, h4, h5, h6,
p, figure, blockquote, dl, dd {
margin: 0;
}
ul, ol {
margin: 0;
padding: 0;
}
img,
picture,
video,
canvas,
svg {
display: block;
max-width: 100%;
height: auto;
}
input,
button,
textarea,
select {
font: inherit;
color: inherit;
}
button {
cursor: pointer;
}
:focus-visible {
@include focus-ring;
}
@media (prefers-reduced-motion: reduce) {
*,
*::before,
*::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
scroll-behavior: auto !important;
}
}
+23
View File
@@ -0,0 +1,23 @@
// -----------------------------------------------------------------------------
// Emit every token as a CSS custom property.
//
// This is the only place the SASS maps become CSS. Everything downstream reads
// `var(--tlig-…)`, which means a language, a section or a future dark mode can
// re-point a token in one declaration block instead of re-stating the rules.
// -----------------------------------------------------------------------------
@use 'sass:map';
@use 'sass:list';
@use '../abstracts' as *;
:root {
@each $key, $value in $colors { --#{$prefix}-color-#{$key}: #{$value}; }
@each $key, $value in $fonts { --#{$prefix}-font-#{$key}: #{list.join((), $value, $separator: comma)}; }
@each $key, $value in $font-sizes { --#{$prefix}-size-#{$key}: #{$value}; }
@each $key, $value in $line-heights{ --#{$prefix}-leading-#{$key}: #{$value}; }
@each $key, $value in $font-weights{ --#{$prefix}-weight-#{$key}: #{$value}; }
@each $key, $value in $space { --#{$prefix}-space-#{$key}: #{$value}; }
@each $key, $value in $radii { --#{$prefix}-radius-#{$key}: #{$value}; }
--#{$prefix}-site-width: #{$site-width};
}
+103
View File
@@ -0,0 +1,103 @@
// -----------------------------------------------------------------------------
// Element typography.
//
// Sizes and colours are the ones the site renders today; the difference is that
// each is stated once, from a token, with no `!important`. `tlig_en.css` sets
// h2 globally with four `!important`s to beat Avada's own h2 rule — here there
// is only one h2 rule, so nothing needs beating.
// -----------------------------------------------------------------------------
@use '../abstracts' as *;
body {
@include type('body', 'base', 'normal');
color: color('page-ink');
letter-spacing: 0;
}
h1 {
@include heading('2xl');
margin-block: space('md') space('xs');
}
// Trebuchet, deep red, bold: the message-heading look, applied globally today.
h2 {
font-family: font('message');
font-size: size('lg');
line-height: leading('normal');
font-weight: weight('bold');
color: color('heading-message');
margin-block-end: space('sm');
}
h3 {
@include type('body', 'md', 'normal', 'bold');
color: color('heading');
margin-block-end: space('xs');
}
h4 {
@include heading('xl');
margin-block: space('lg');
}
h5 {
@include type('body', 'sm', 'normal', 'bold');
color: color('heading');
}
h6 {
@include type('body', 'xs', 'normal', 'bold');
color: color('heading');
text-transform: uppercase;
letter-spacing: 0.05em;
}
p {
margin-block-end: space('md');
&:last-child {
margin-block-end: 0;
}
}
strong, b { font-weight: weight('bold'); }
em, i { font-style: italic; }
small {
font-size: size('sm');
color: color('page-ink-muted');
}
hr {
height: 1px;
margin-block: space('xl');
border: 0;
background-color: color('page-rule');
}
blockquote {
@include panel('lg', 'lg');
border-inline-start: 4px solid color('heading-message');
border-end-start-radius: space('2xl');
font-style: italic;
}
// -- Utility classes ---------------------------------------------------------
//
// The live CSS carries `.font-papyrus`, `.font34`, `.centre`, `.bold`, `.ita`
// and friends, each an `!important` one-liner. Those are template-facing, so
// they are kept — but as plain, unforced declarations.
.font-display { font-family: font('display'); }
.font-body { font-family: font('body'); }
.font-message { font-family: font('message'); }
.text-center { text-align: center; }
.text-start { text-align: start; }
.text-justify { text-align: justify; }
.text-muted { color: color('page-ink-muted'); }
.text-strong { color: color('page-ink-strong'); }
.visually-hidden { @include visually-hidden; }
+59
View File
@@ -0,0 +1,59 @@
// -----------------------------------------------------------------------------
// Buttons.
//
// The live site has `#footer-viewMagazineSpan`, `.btnSubmitNewsletter`,
// `.btnSubmitDailyMessage` and `.footer-donateSpan` — four ids and classes
// repeating the same six declarations with different corner radii. One class
// with modifiers replaces them.
// -----------------------------------------------------------------------------
@use '../abstracts' as *;
.button {
display: inline-block;
padding: space('xs') space('md');
border: 1px solid transparent;
border-radius: radius('sm');
background-color: color('button-bg');
color: color('button-ink');
font-family: font('body');
font-size: size('sm');
font-weight: weight('bold');
line-height: 1.2;
letter-spacing: 2px;
text-align: center;
text-decoration: none;
transition: color 120ms ease-in-out, background-color 120ms ease-in-out;
&:hover,
&:focus-visible {
color: color('button-ink-hover');
}
// Donate: gold ground, ink on hover rather than gold-on-gold.
&--donate {
background-color: color('button-donate-bg');
color: color('button-donate-ink');
&:hover,
&:focus-visible {
color: color('page-ink-strong');
}
}
// The half of a joined field + button pair.
&--attached {
border-start-start-radius: 0;
border-end-start-radius: 0;
}
&--block {
display: block;
width: 100%;
}
&[disabled] {
opacity: 0.5;
cursor: not-allowed;
}
}
+36
View File
@@ -0,0 +1,36 @@
// -----------------------------------------------------------------------------
// Text inputs. Modelled on the footer newsletter field (`.inputEmail`), which
// today carries five `!important`s to override Avada's form styling.
// -----------------------------------------------------------------------------
@use '../abstracts' as *;
.field {
min-width: 0;
padding: space('2xs') space('xs');
border: 1px solid color('field-bg');
border-radius: radius('sm');
background-color: color('field-bg');
color: color('field-ink');
font-family: font('body');
font-size: size('sm');
letter-spacing: 2px;
&::placeholder {
color: color('field-placeholder');
opacity: 1;
}
&--attached {
border-start-end-radius: 0;
border-end-end-radius: 0;
}
}
// Field + button as one control.
.field-group {
display: flex;
justify-content: center;
.field { flex: 1 1 auto; max-width: 18rem; }
}
+116
View File
@@ -0,0 +1,116 @@
// -----------------------------------------------------------------------------
// Footer.
//
// The live footer is 24 id selectors (`#footer-magazine`, `#footer-rightblock`,
// `#footer-whatOptionLink1`…) with hand-tuned percentage widths and a media
// query that resets each one to 100%. This is the same layout expressed as two
// flex containers, and it re-points the link tokens once via `.on-dark` instead
// of colouring each link by hand.
// -----------------------------------------------------------------------------
@use '../abstracts' as *;
.site-footer {
// Scope the dark-ground tokens to the whole region. Everything inside —
// links, headings, buttons — reads the right colour without a single
// footer-specific colour declaration. Shared with `.on-dark` so the two can
// never drift apart.
@include dark-ground;
padding-block: space('xl');
background-color: color('footer-bg');
font-family: font('display');
}
.site-footer__inner {
width: min(100% - 2 * #{space-raw('lg')}, var(--#{$prefix}-site-width));
margin-inline: auto;
}
// Two rows: magazine + newsletter above, the three link columns below.
.site-footer__row {
display: flex;
flex-wrap: wrap;
gap: space('xl');
justify-content: space-between;
& + & {
margin-block-start: space('2xl');
}
}
.site-footer__col {
flex: 1 1 18rem;
text-align: center;
}
// The magazine block is the wide one at desktop; below the tablet breakpoint
// every column is simply full width, which the flex-basis already gives us.
.site-footer__col--wide {
flex: 1 1 28rem;
}
.site-footer__title {
font-family: font('display');
font-size: size('md');
line-height: leading('tight');
color: color('footer-ink');
margin-block-end: space('sm');
}
.site-footer__text {
font-size: size('md');
line-height: leading('snug');
}
// Social row.
.site-footer__social {
display: flex;
flex-wrap: wrap;
gap: space('md');
justify-content: center;
list-style: none;
a {
font-size: size('xl');
&:hover,
&:focus-visible {
color: color('button-donate-bg');
}
}
}
// The "what is…" dropdown: a bordered list on the dark panel colour.
.site-footer__menu {
// 80% of the column, as the live `#footer-whatDropdown` has it, but capped so
// it does not stretch on a wide screen.
max-width: min(22rem, 80%);
margin-inline: auto;
list-style: none;
text-align: start;
border: 1px solid color('footer-rule');
border-radius: radius('sm');
overflow: hidden;
li + li {
border-block-start: 1px solid color('footer-rule');
}
a {
display: block;
padding: space('xs') space('sm');
background-color: color('footer-panel');
}
}
@include down('tablet') {
.site-footer__row {
gap: space('lg');
}
.site-footer__col,
.site-footer__col--wide {
flex-basis: 100%;
}
}
+3
View File
@@ -0,0 +1,3 @@
@forward 'button';
@forward 'field';
@forward 'footer';
+14
View File
@@ -0,0 +1,14 @@
// -----------------------------------------------------------------------------
// TLIG stylesheet — entry point.
//
// Layer order is load-bearing: tokens first, then element defaults, then
// components. Nothing later in the file needs higher specificity than anything
// earlier, which is the whole reason there is not a single `!important` here.
//
// abstracts/ compile-time only, emits no CSS
// base/ custom properties, @font-face, reset, element typography, links
// components/ single-purpose blocks
// -----------------------------------------------------------------------------
@use 'base';
@use 'components';
+69
View File
@@ -0,0 +1,69 @@
import { defineConfig } from 'vite'
import { resolve } from 'node:path'
const here = (...p) => resolve(import.meta.dirname, ...p)
// Two build targets from one config:
//
// vite build → dist/ the deliverable: tlig.css + fonts,
// no HTML. This is what WordPress
// enqueues.
// vite build --mode demo → demo-dist/ a static demo page that loads that
// stylesheet through a real <link>,
// with the site images beside it.
//
// The second exists because in dev Vite injects CSS through JavaScript, so
// anything with a `transition` animates from its unstyled state on first paint.
// That is a dev artifact, but it makes the demo misrepresent the stylesheet.
// The static build paints once, the way a browser will on the real site.
export default defineConfig(({ mode }) => {
const isDemo = mode === 'demo'
return {
root: 'src',
// Relative asset URLs — a url() in CSS resolves against the stylesheet's
// own location, so the output can be dropped at any path under wp-content
// without rebuilding.
base: './',
// `site/` mirrors the live asset tree, so the demo can reference
// /wp-content/includes/images/… exactly as a WordPress template does.
publicDir: here('site'),
css: {
devSourcemap: true,
},
build: {
outDir: isDemo ? here('demo-dist') : here('dist'),
emptyOutDir: true,
// The demo needs the wood and scroll images; the deliverable does not —
// WordPress already hosts them.
copyPublicDir: isDemo,
sourcemap: true,
cssCodeSplit: false,
rollupOptions: {
// The demo is built from the page; the deliverable from the entry
// module, so no demo markup or demo CSS can reach dist/.
input: isDemo ? here('src/index.html') : here('src/main.js'),
output: {
// Predictable names: WordPress enqueues these paths, so they must not
// change on every build. Fonts keep their own names for the same
// reason — a preload hint in the theme header has to stay valid.
entryFileNames: isDemo ? 'assets/[name]-[hash].js' : 'tlig.js',
assetFileNames: (info) => {
const name = info.names?.[0] ?? info.name ?? ''
if (name.endsWith('.css')) return 'tlig.css'
if (/\.(woff2?|ttf|eot|svg)$/.test(name)) return 'fonts/[name][extname]'
return 'assets/[name]-[hash][extname]'
},
},
},
},
}
})