Compare commits
30 Commits
fec0327005
..
v1.2.1
| Author | SHA1 | Date | |
|---|---|---|---|
| 8bb782287e | |||
| 56ec85c17e | |||
| 9412c20248 | |||
| 65f97d2707 | |||
| 564070b270 | |||
| ad90451846 | |||
| 22c2ef1b26 | |||
| 301120c174 | |||
| 1a0ffead5a | |||
| 719956341c | |||
| 2a1e6dc8a4 | |||
| 6b19a55655 | |||
| a2b9d445e4 | |||
| a14306cce4 | |||
| 43f42a8274 | |||
| 8376860fb4 | |||
| d1803f06dd | |||
| ea16d1b21b | |||
| 99084cc597 | |||
| d296d88c4c | |||
| 17d10ce1a0 | |||
| 7549aa6c90 | |||
| 6707504b88 | |||
| e5dd090c45 | |||
| eb0186a5b8 | |||
| b28ec41ccb | |||
| d2bac6b8d3 | |||
| a2c6408277 | |||
| 3b5c304002 | |||
| 59b8d969df |
@@ -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
|
||||
@@ -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@34e114876b0b11c390a56381ad16ebd13914f8d5 # 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@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3
|
||||
|
||||
- name: Set up Buildx
|
||||
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3
|
||||
|
||||
- name: Log in to the Gitea container registry
|
||||
if: github.event_name != 'pull_request'
|
||||
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.PACKAGES_TOKEN }}
|
||||
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # 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"
|
||||
@@ -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
|
||||
@@ -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. |
|
||||
@@ -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
@@ -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
@@ -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",
|
||||
];
|
||||
@@ -0,0 +1,53 @@
|
||||
{
|
||||
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
||||
"extends": [
|
||||
"config:recommended",
|
||||
":dependencyDashboard",
|
||||
":semanticCommits"
|
||||
],
|
||||
"labels": ["renovate"],
|
||||
"semanticCommits": "enabled",
|
||||
"semanticCommitType": "fix",
|
||||
"semanticCommitScope": "deps",
|
||||
"github-actions": {
|
||||
"managerFilePatterns": ["/^\\.gitea/workflows/[^/]+\\.ya?ml$/"]
|
||||
},
|
||||
"packageRules": [
|
||||
{
|
||||
"description": "Group nginx base image updates",
|
||||
"matchManagers": ["dockerfile"],
|
||||
"groupName": "docker base image"
|
||||
},
|
||||
{
|
||||
"description": "Commit every update as fix(deps) so each merged Renovate PR triggers a patch release. config:recommended pulls in :semanticPrefixFixDepsChoreOthers, which forces non-npm updates (Docker, Actions) to chore and would otherwise produce no release.",
|
||||
"matchPackageNames": ["*"],
|
||||
"semanticCommitType": "fix"
|
||||
},
|
||||
{
|
||||
"description": "Auto-merge patch and minor updates once checks pass.",
|
||||
"matchUpdateTypes": ["patch", "minor"],
|
||||
"automerge": true
|
||||
}
|
||||
],
|
||||
"platformAutomerge": true,
|
||||
"customManagers": [
|
||||
{
|
||||
"customType": "regex",
|
||||
"description": "Update HTML dependencies annotated with a renovate comment, e.g. <!-- renovate: datasource=npm depName=bootstrap --> before the versioned URL",
|
||||
"managerFilePatterns": ["/\\.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",
|
||||
"managerFilePatterns": ["/\\.html$/"],
|
||||
"matchStrings": [
|
||||
"https://cdn\\.jsdelivr\\.net/npm/(?<depName>@?[^@/]+(?:/[^@/]+)?)@(?<currentValue>\\d[^/\"']+)",
|
||||
"https://unpkg\\.com/(?<depName>@?[^@/]+(?:/[^@/]+)?)@(?<currentValue>\\d[^/\"']+)"
|
||||
],
|
||||
"datasourceTemplate": "npm"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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
@@ -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: #c8102e;
|
||||
--surface: #002b5c;
|
||||
--text: #f5f0e1;
|
||||
--message: #f5f0e1;
|
||||
--button-bg: #002b5c;
|
||||
--button-text: #f5f0e1;
|
||||
--button-active-bg: #f5f0e1;
|
||||
--button-active-text: #002b5c;
|
||||
}
|
||||
|
||||
* {
|
||||
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);
|
||||
}
|
||||
Reference in New Issue
Block a user