24 Commits

Author SHA1 Message Date
lyrathorpe 65f97d2707 Merge pull request 'feat: present theme switch as a segmented control' (#15) from feat/segmented-theme-switch into main
Build and publish container / build (push) Successful in 6m7s
Reviewed-on: #15
2026-06-12 11:45:08 +01:00
Emma Thorpe ad90451846 feat: present theme switch as a segmented control
Build and publish container / build (pull_request) Successful in 4m49s
Join the two theme options into a single rounded container with the active
segment filled and a divider between them, replacing the two separate pills.
Markup and aria-pressed behaviour are unchanged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-12 11:34:32 +01:00
renovate-bot 1a0ffead5a Merge pull request 'chore(deps): update docker base image to v1.31' (#8) from renovate/docker-base-image into main
Build and publish container / build (push) Successful in 6m21s
2026-06-12 01:14:42 +01:00
Renovate Bot 719956341c chore(deps): update docker base image to v1.31
Build and publish container / build (pull_request) Successful in 4m38s
2026-06-12 00:02:27 +00:00
lyrathorpe 2a1e6dc8a4 Merge pull request 'feat(messages): add initial messages' (#6) from feat/reasons-why into main
Build and publish container / build (push) Successful in 5m59s
Reviewed-on: #6
2026-06-11 21:08:15 +01:00
lyrathorpe 6b19a55655 feat(messages): add initial messages
Build and publish container / build (pull_request) Successful in 4m22s
more added later on
2026-06-11 21:03:31 +01:00
lyrathorpe a2b9d445e4 Merge pull request 'Fix/theme colours' (#5) from fix/theme-colours into main
Build and publish container / build (push) Successful in 6m18s
Reviewed-on: #5
2026-06-11 17:49:40 +01:00
Emma Thorpe a14306cce4 fix: recolour original theme to a cream field with blue text
Build and publish container / build (pull_request) Successful in 4m51s
Use a cream/white background with navy message text and red buttons, so the
original red/white/blue livery presents blue text rather than white text on a
blue field.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-11 17:41:55 +01:00
Emma Thorpe 43f42a8274 fix: use solid theme backgrounds instead of a gradient
Replace the body background gradient with the solid theme colour and remove
the now-unused --bg-accent variables from both colour schemes.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-11 17:36:47 +01:00
lyrathorpe 8376860fb4 Merge pull request 'fix: build multi-arch images for amd64 and arm64' (#4) from fix/multi-arch-build into main
Build and publish container / build (push) Successful in 6m51s
Reviewed-on: #4
2026-06-11 17:26:57 +01:00
Emma Thorpe d1803f06dd fix: build multi-arch images for amd64 and arm64
Build and publish container / build (pull_request) Successful in 5m2s
Add QEMU setup and build for linux/amd64 and linux/arm64 (armv8), publishing
a single multi-arch manifest. The nginx-unprivileged base image provides both
architectures.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-11 17:20:20 +01:00
lyrathorpe ea16d1b21b Merge pull request 'ci: tag images by semver and point latest at newest release' (#3) from ci/image-tagging into main
Build and publish container / build (push) Successful in 3m32s
Reviewed-on: #3
2026-06-11 17:10:26 +01:00
Emma Thorpe 99084cc597 feat: auto-release images from conventional commits on main
Build and publish container / build (pull_request) Successful in 2m58s
On each push to main, derive the next semantic version from the
conventional-commit messages since the last v* tag (feat -> minor,
fix/perf -> patch, \! or BREAKING CHANGE -> major) and, when a release is
warranted, build and publish the image tagged X.Y.Z, X.Y, X and latest,
then record an annotated vX.Y.Z tag for the next computation. Non-release
pushes publish a sha-<short> image only.

Configure Renovate to commit updates as fix(deps): so each merged Renovate
PR registers as a patch change and is released and tagged automatically.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-11 17:04:40 +01:00
Emma Thorpe d296d88c4c ci: tag images by semver and point latest at newest release
Build and publish container / build (pull_request) Successful in 1m59s
Replace the raw latest-on-default-branch tag, which moved latest on every
main push, with metadata-action's latest=auto flavor so latest follows the
newest non-prerelease v* release. Add a {{major}} tag alongside the
existing version and major.minor semver tags; branch and SHA tags remain
for traceability of non-release builds.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-11 16:43:33 +01:00
lyrathorpe 17d10ce1a0 Merge pull request 'fix: authenticate to container registry with a package-scoped PAT' (#2) from fix/ci-registry-auth into main
Build and publish container / build (push) Successful in 2m39s
Reviewed-on: #2
2026-06-11 16:36:30 +01:00
Emma Thorpe 7549aa6c90 fix: authenticate to container registry with a package-scoped PAT
Build and publish container / build (pull_request) Successful in 2m7s
The auto-provided GITEA_TOKEN does not carry container registry write
permission on most Gitea instances, causing docker login to fail with
"unauthorized". Use a Personal Access Token supplied via the
PACKAGES_TOKEN secret, with the package namespace owner as the username.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-11 16:31:32 +01:00
lyrathorpe 6707504b88 Merge pull request 'Feat/why is the dlr shut' (#1) from feat/why-is-the-dlr-shut into main
Build and publish container / build (push) Failing after 40s
Reviewed-on: #1
2026-06-11 16:26:33 +01:00
Emma Thorpe e5dd090c45 docs: document container, CI and dependency updates
Build and publish container / build (pull_request) Successful in 5m18s
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-11 16:18:04 +01:00
Emma Thorpe eb0186a5b8 build: add Renovate configuration
Track the Dockerfile base image, the actions used in the Gitea workflow,
and versioned front-end dependencies referenced in HTML (via renovate
comment annotations or jsDelivr/unpkg npm URLs).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-11 16:18:04 +01:00
Emma Thorpe b28ec41ccb ci: build and publish the container via Gitea Actions
Build the image on pushes to main, version tags and pull requests, and
push to the Gitea container registry (except on PRs) using the
auto-provided GITEA_TOKEN. Tags are derived with docker/metadata-action.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-11 16:18:04 +01:00
Emma Thorpe d2bac6b8d3 feat: containerise the site with nginx-unprivileged
Serve the static site from a non-root nginx image listening on 8080,
with cache headers, gzip and a /healthz endpoint. Designed to run behind
an external reverse proxy that terminates TLS.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-11 16:18:04 +01:00
Emma Thorpe a2c6408277 docs: add README
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-11 16:18:04 +01:00
Emma Thorpe 3b5c304002 feat: add random message selection and theme toggle
script.js picks a random entry from MESSAGES on load and on each "Check
again" click, and persists the chosen colour scheme in localStorage.
messages.js holds the MESSAGES array as a placeholder template to be
filled in with content.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-11 16:18:04 +01:00
Emma Thorpe 59b8d969df feat: add page structure and DLR colour schemes
Single-page layout with a centred message, a colour-scheme toggle, and a
"Check again" button. styles.css defines two palettes selected via the
data-theme attribute: modern DLR turquoise/teal and the original 1987
red-and-blue livery.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-11 16:18:04 +01:00
10 changed files with 609 additions and 0 deletions
+12
View File
@@ -0,0 +1,12 @@
# Keep the build context minimal; only the site assets and nginx config
# referenced by the Dockerfile are needed.
.git
.gitea
.github
.idea
.vscode
*.md
renovate.json
.renovaterc*
Dockerfile
.dockerignore
+132
View File
@@ -0,0 +1,132 @@
name: Build and publish container
on:
push:
branches: [main]
pull_request:
branches: [main]
defaults:
run:
shell: bash
jobs:
build:
runs-on: ubuntu-latest
permissions:
contents: write
packages: write
steps:
- name: Checkout
uses: actions/checkout@v4
with:
# Full history and tags are required to derive the next version
# from the conventional-commit messages since the last release.
fetch-depth: 0
- name: Determine registry host
run: echo "REGISTRY=${GITHUB_SERVER_URL#*://}" >> "$GITHUB_ENV"
# Derive the release version from conventional commits since the last
# v* tag: feat -> minor, fix/perf -> patch, ! or BREAKING CHANGE -> major.
# Anything else (chore, ci, docs, build) produces no release; those builds
# are published under a sha-<short> tag only.
- name: Compute version and image tags
id: version
run: |
set -euo pipefail
image="${REGISTRY}/${GITHUB_REPOSITORY,,}"
last_tag="$(git tag --list 'v*' --sort=-v:refname | head -n1 || true)"
if [ -n "$last_tag" ]; then
range="${last_tag}..HEAD"
base="${last_tag#v}"
else
range=""
base="0.0.0"
fi
subjects="$(git log ${range} --format='%s')"
bodies="$(git log ${range} --format='%B')"
bump="none"
if printf '%s\n' "$bodies" | grep -qiE 'BREAKING[ -]CHANGE' \
|| printf '%s\n' "$subjects" | grep -qE '^[a-z]+([(][^)]*[)])?!:'; then
bump="major"
elif printf '%s\n' "$subjects" | grep -qE '^feat([(][^)]*[)])?:'; then
bump="minor"
elif printf '%s\n' "$subjects" | grep -qE '^(fix|perf)([(][^)]*[)])?:'; then
bump="patch"
fi
major="${base%%.*}"
rest="${base#*.}"
minor="${rest%%.*}"
patch="${rest##*.}"
release="false"
if [ "${GITHUB_EVENT_NAME}" = "push" ] && [ "$bump" != "none" ]; then
release="true"
case "$bump" in
major) major=$((major + 1)); minor=0; patch=0 ;;
minor) minor=$((minor + 1)); patch=0 ;;
patch) patch=$((patch + 1)) ;;
esac
version="${major}.${minor}.${patch}"
{
echo "tags<<__EOT__"
echo "${image}:${version}"
echo "${image}:${major}.${minor}"
echo "${image}:${major}"
echo "${image}:latest"
echo "__EOT__"
} >> "$GITHUB_OUTPUT"
echo "version=${version}" >> "$GITHUB_OUTPUT"
else
short="$(git rev-parse --short HEAD)"
{
echo "tags<<__EOT__"
echo "${image}:sha-${short}"
echo "__EOT__"
} >> "$GITHUB_OUTPUT"
fi
echo "release=${release}" >> "$GITHUB_OUTPUT"
echo "Computed bump=${bump}, release=${release}, base=${base}"
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Buildx
uses: docker/setup-buildx-action@v3
- name: Log in to the Gitea container registry
if: github.event_name != 'pull_request'
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.repository_owner }}
password: ${{ secrets.PACKAGES_TOKEN }}
- name: Build and push
uses: docker/build-push-action@v6
with:
context: .
platforms: linux/amd64,linux/arm64
push: ${{ github.event_name != 'pull_request' }}
tags: ${{ steps.version.outputs.tags }}
labels: |
org.opencontainers.image.source=${{ github.server_url }}/${{ github.repository }}
org.opencontainers.image.revision=${{ github.sha }}
# Record the release as an annotated git tag so the next run computes the
# following version from it. This push does not re-trigger the workflow,
# which only listens on the main branch and pull requests.
- name: Tag the release
if: steps.version.outputs.release == 'true'
run: |
set -euo pipefail
v="v${{ steps.version.outputs.version }}"
git config user.name "${{ github.actor }}"
git config user.email "${{ github.actor }}@users.noreply.${REGISTRY}"
git tag -a "$v" -m "$v"
git push origin "$v"
+9
View File
@@ -0,0 +1,9 @@
# Lightweight, non-root nginx serving the static site.
# Runs as user "nginx" and listens on 8080, ready to sit behind an
# external reverse proxy that terminates TLS and forwards requests.
FROM nginxinc/nginx-unprivileged:1.31-alpine-slim@sha256:6616de6eaa82bc2ee3541fa287a8fca7dc7271e6374e9402014dbd13f4a980ae
COPY default.conf /etc/nginx/conf.d/default.conf
COPY --chown=nginx:nginx index.html styles.css script.js messages.js /usr/share/nginx/html/
EXPOSE 8080
+114
View File
@@ -0,0 +1,114 @@
# Why is the DLR shut today?
A single-page site that displays one randomly chosen message in the centre of
the screen. The message changes on every page load and whenever the
**Check again** button is pressed.
The site is themed around the Docklands Light Railway colour scheme, with a
toggle between:
- **Modern colours** — the current DLR turquoise/teal branding.
- **Original colours** — the 1987 DLR red-and-blue livery.
The chosen theme is remembered between visits via `localStorage`.
## Adding messages
Edit `messages.js` and fill the `MESSAGES` array with your own reasons — one
string per entry. Entries are inserted as plain text. Until you add some, the
page shows a fallback prompt.
## Running
It is a static site with no build step. Open `index.html` in a browser, or
serve the directory with any static file server, for example:
```sh
python3 -m http.server
```
## Container
The site is packaged as a container based on `nginxinc/nginx-unprivileged`. It
runs as a non-root user and listens on port **8080**, serving the static files
and exposing a `/healthz` endpoint. It is designed to sit behind an external
reverse proxy that terminates TLS and routes by host.
Build and run locally:
```sh
docker build -t dlr .
docker run --rm -p 8080:8080 dlr
# then browse http://localhost:8080
```
## CI
`.gitea/workflows/build-and-publish.yml` builds the container with Gitea Actions
on every push to `main` and on pull requests. Pull requests build the image but
do not push. The registry host is derived from the Gitea server URL. Images are
built for `linux/amd64` and `linux/arm64` (armv8) and published as a single
multi-arch manifest; the arm64 build runs under QEMU emulation.
Authentication requires a Personal Access Token with package read/write scope,
because the automatically provided `GITEA_TOKEN` does not carry container
registry write permission on most Gitea instances. Create the token under an
account with write access to the target package namespace, then store it as a
repository Actions secret named `PACKAGES_TOKEN`.
### Automatic releases
The published image is `<gitea-host>/<owner>/<repo>`. Releases are derived from
[Conventional Commits](https://www.conventionalcommits.org/). On each push to
`main`, the workflow inspects the commits since the last `v*` tag and computes
the next version:
- `feat:` → minor bump,
- `fix:` / `perf:` → patch bump,
- `!` or `BREAKING CHANGE` → major bump,
- anything else (`chore`, `ci`, `docs`, `build`) → no release.
When a release is warranted, the image is published with `X.Y.Z`, `X.Y`, `X` and
`latest` tags, and the workflow records an annotated `vX.Y.Z` git tag so the next
release is computed from it. Pushes to `main` that warrant no release are
published under a `sha-<short>` tag only, so `latest` always points at the most
recent release rather than the newest commit.
Recording the release tag requires the workflow's `contents: write` permission;
if the instance forbids the automatic token from pushing, supply a PAT with
repository write scope and push the tag with it instead.
## Dependency updates
`renovate.json` configures Renovate to keep dependencies current:
- the Dockerfile base image,
- the actions used in the Gitea workflow,
- versioned front-end dependencies referenced in HTML.
There are currently no external front-end dependencies. When one is added via a
CDN, Renovate will track it if it is either annotated with a comment, e.g.
```html
<!-- renovate: datasource=npm depName=bootstrap -->
<link href="https://cdn.example.com/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
```
or referenced through a versioned jsDelivr / unpkg npm URL, which is detected
automatically.
Renovate is configured to commit updates as `fix(deps): …`. Each merged Renovate
PR therefore registers as a patch-level change, so the release workflow above
cuts a new patch release and tags the image automatically.
## Files
| File | Purpose |
| ------------------------------------- | ------------------------------------------------ |
| `index.html` | Page structure. |
| `styles.css` | Both colour schemes, selected via `data-theme`. |
| `messages.js` | The list of messages (fill this in). |
| `script.js` | Random message selection and the theme toggle. |
| `Dockerfile` / `default.conf` | Container image and nginx static-serving config. |
| `.gitea/workflows/` | Gitea Actions build-and-publish pipeline. |
| `renovate.json` | Renovate dependency-update configuration. |
+42
View File
@@ -0,0 +1,42 @@
# Static file serving for "Why is the DLR shut today?".
# Intended to run behind an external reverse proxy (e.g. NGINX) which
# handles TLS, host routing and any X-Forwarded-* headers.
server {
listen 8080;
listen [::]:8080;
server_name _;
root /usr/share/nginx/html;
index index.html;
gzip on;
gzip_vary on;
gzip_types text/css application/javascript;
location / {
try_files $uri $uri/ =404;
}
# HTML should always be revalidated so deploys are picked up promptly.
location = /index.html {
add_header Cache-Control "no-cache";
}
# The message list is edited frequently; do not cache it.
location = /messages.js {
add_header Cache-Control "no-cache";
}
# Other static assets may be cached for a short period.
location ~* \.(?:css|js)$ {
add_header Cache-Control "public, max-age=3600";
}
# Health endpoint for the proxy / orchestrator.
location = /healthz {
access_log off;
default_type text/plain;
return 200 "ok\n";
}
}
+31
View File
@@ -0,0 +1,31 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Why is the DLR shut today?</title>
<link rel="stylesheet" href="styles.css">
</head>
<body data-theme="modern">
<header class="controls">
<fieldset class="theme-toggle">
<legend class="sr-only">Colour scheme</legend>
<button type="button" class="theme-button" data-set-theme="modern" aria-pressed="true">
Modern colours
</button>
<button type="button" class="theme-button" data-set-theme="original" aria-pressed="false">
Original colours
</button>
</fieldset>
</header>
<main class="stage">
<h1 class="title">Why is the DLR shut today?</h1>
<p class="message" id="message" role="status" aria-live="polite"></p>
<button type="button" class="refresh-button" id="refresh">Check again</button>
</main>
<script src="messages.js"></script>
<script src="script.js"></script>
</body>
</html>
+23
View File
@@ -0,0 +1,23 @@
/* Messages for "Why is the DLR shut today?"
*
* Fill this array with your own reasons. One string per entry.
* On every page load (and every "Check again" click) one entry is
* picked at random and shown in the centre of the screen.
*
* Keep them short — roughly a sentence — so they fit the large display
* text. HTML is NOT rendered; entries are inserted as plain text.
*
* Replace the placeholders below with your own content.
*/
const MESSAGES = [
"Maggie came back, she was unimpressed",
"They mixed up the B23s and the 2024 tube stock",
"The computer went on strike",
"Leaves on the track",
"Escalators broke at Cutty Sark",
"EHRC decided it was woke",
"JK Rowling",
"Kaiju",
"28 Days Later happened",
];
+42
View File
@@ -0,0 +1,42 @@
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": [
"config:recommended",
":dependencyDashboard",
":semanticCommits"
],
"labels": ["renovate"],
"semanticCommits": "enabled",
"semanticCommitType": "fix",
"semanticCommitScope": "deps",
"github-actions": {
"fileMatch": ["^\\.gitea/workflows/[^/]+\\.ya?ml$"]
},
"packageRules": [
{
"description": "Group nginx base image updates",
"matchManagers": ["dockerfile"],
"groupName": "docker base image"
}
],
"customManagers": [
{
"customType": "regex",
"description": "Update HTML dependencies annotated with a renovate comment, e.g. <!-- renovate: datasource=npm depName=bootstrap --> before the versioned URL",
"fileMatch": ["\\.html$"],
"matchStrings": [
"datasource=(?<datasource>\\S+) depName=(?<depName>\\S+)( versioning=(?<versioning>\\S+))?[\\s\\S]*?(?<currentValue>v?\\d+\\.\\d+\\.\\d+[\\w.-]*)"
]
},
{
"customType": "regex",
"description": "Auto-detect versioned jsDelivr / unpkg npm assets in HTML",
"fileMatch": ["\\.html$"],
"matchStrings": [
"https://cdn\\.jsdelivr\\.net/npm/(?<depName>@?[^@/]+(?:/[^@/]+)?)@(?<currentValue>\\d[^/\"']+)",
"https://unpkg\\.com/(?<depName>@?[^@/]+(?:/[^@/]+)?)@(?<currentValue>\\d[^/\"']+)"
],
"datasourceTemplate": "npm"
}
]
}
+59
View File
@@ -0,0 +1,59 @@
/* Why is the DLR shut today?
* Picks a random message on load and on demand, and handles the
* colour-scheme toggle. Messages live in messages.js (MESSAGES). */
(function () {
"use strict";
const FALLBACK = "Add some reasons in messages.js";
const messageEl = document.getElementById("message");
const refreshButton = document.getElementById("refresh");
const themeButtons = document.querySelectorAll("[data-set-theme]");
const THEME_KEY = "dlr-theme";
function pickMessage() {
if (!Array.isArray(MESSAGES) || MESSAGES.length === 0) {
return FALLBACK;
}
const index = Math.floor(Math.random() * MESSAGES.length);
return MESSAGES[index];
}
function showMessage() {
messageEl.textContent = pickMessage();
}
function applyTheme(theme) {
document.body.setAttribute("data-theme", theme);
themeButtons.forEach(function (button) {
const isActive = button.dataset.setTheme === theme;
button.setAttribute("aria-pressed", String(isActive));
});
try {
localStorage.setItem(THEME_KEY, theme);
} catch (err) {
/* localStorage unavailable; theme simply won't persist. */
}
}
function initTheme() {
let saved = null;
try {
saved = localStorage.getItem(THEME_KEY);
} catch (err) {
/* ignore */
}
applyTheme(saved === "original" ? "original" : "modern");
}
themeButtons.forEach(function (button) {
button.addEventListener("click", function () {
applyTheme(button.dataset.setTheme);
});
});
refreshButton.addEventListener("click", showMessage);
initTheme();
showMessage();
})();
+145
View File
@@ -0,0 +1,145 @@
/* Why is the DLR shut today?
Two colour schemes selected via the [data-theme] attribute on <body>:
- modern: current DLR turquoise/teal branding
- original: 1987 DLR red-and-blue livery */
:root {
--font-stack: "Helvetica Neue", Helvetica, Arial, sans-serif;
}
/* Modern DLR — turquoise/teal */
[data-theme="modern"] {
--bg: #00afaa;
--surface: #ffffff;
--text: #ffffff;
--message: #ffffff;
--button-bg: #ffffff;
--button-text: #007e7a;
--button-active-bg: #00302e;
--button-active-text: #ffffff;
}
/* Original DLR — 1987 red, white and blue */
[data-theme="original"] {
--bg: #f5f0e1;
--surface: #002b5c;
--text: #002b5c;
--message: #002b5c;
--button-bg: #c8102e;
--button-text: #f5f0e1;
--button-active-bg: #002b5c;
--button-active-text: #f5f0e1;
}
* {
box-sizing: border-box;
}
body {
margin: 0;
min-height: 100vh;
display: flex;
flex-direction: column;
font-family: var(--font-stack);
color: var(--text);
background: var(--bg);
transition: background 0.4s ease, color 0.4s ease;
}
.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
border: 0;
}
.controls {
display: flex;
justify-content: flex-end;
padding: 1.5rem;
}
/* Segmented control: both options joined in one rounded container,
with the active segment filled. */
.theme-toggle {
display: inline-flex;
border: 2px solid var(--button-bg);
border-radius: 999px;
overflow: hidden;
margin: 0;
padding: 0;
}
.theme-button {
font-family: inherit;
font-size: 0.9rem;
font-weight: 600;
padding: 0.5rem 1.1rem;
border: none;
background: transparent;
color: var(--text);
cursor: pointer;
transition: background 0.2s ease, color 0.2s ease;
}
/* Divider between the two segments. */
.theme-button + .theme-button {
border-left: 2px solid var(--button-bg);
}
.theme-button[aria-pressed="true"] {
background: var(--button-active-bg);
color: var(--button-active-text);
}
.stage {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
text-align: center;
padding: 2rem;
gap: 2rem;
}
.title {
font-size: clamp(1.5rem, 4vw, 2.5rem);
font-weight: 700;
margin: 0;
letter-spacing: 0.02em;
}
.message {
font-size: clamp(1.75rem, 6vw, 4rem);
font-weight: 800;
line-height: 1.2;
margin: 0;
max-width: 22ch;
color: var(--message);
}
.refresh-button {
font-family: inherit;
font-size: 1rem;
font-weight: 600;
padding: 0.75rem 1.75rem;
border: 2px solid var(--button-bg);
border-radius: 999px;
background: var(--button-bg);
color: var(--button-text);
cursor: pointer;
transition: transform 0.1s ease, opacity 0.2s ease;
}
.refresh-button:hover {
opacity: 0.9;
}
.refresh-button:active {
transform: scale(0.97);
}