diff --git a/docs/guides/admin-general-settings.md b/docs/guides/admin-general-settings.md
index df3f3b3..d617c83 100644
--- a/docs/guides/admin-general-settings.md
+++ b/docs/guides/admin-general-settings.md
@@ -2,8 +2,8 @@
type: guide
title: Admin — General Settings
description: How an admin edits required platform-wide settings, including the validated event window, from the /admin/general page.
-tags: [guide, admin, settings, general, tester, datetime, validation]
-timestamp: 2026-07-22T14:50:25Z
+tags: [guide, admin, settings, general, tester, datetime, validation, utc, timezone]
+timestamp: 2026-07-22T15:09:39Z
---
# When this view is available
@@ -56,7 +56,8 @@ The page is a single reactive form with these controls (every
* **Initial load:** `loading() === true` renders `general-loading`. After
both requests resolve, the form is patched with the values from the
backend (UTC timestamps are converted to `datetime-local` strings via
- `toDatetimeLocal` so the native picker shows them).
+ `toDatetimeLocal` so the native picker shows them — see
+ [Datetime UTC round-trip](#datetime-utc-round-trip) below).
* **Logo upload:** selecting a file fires `POST /api/v1/uploads/logo`,
then writes the returned `publicUrl` into the hidden `logo` control.
If the upload fails, `general-logo-error` shows the message; the
@@ -179,6 +180,62 @@ is rendered inside the per-field error region
(`data-testid="general-eventEnd-error"`) by the
`eventEndFieldMessage` helper.
+# Datetime UTC round-trip
+
+Both Event-window fields (`general-eventStart`, `general-eventEnd`) are
+backed by HTML `` controls, which the
+browser renders as a wall-clock picker **without** any timezone
+indicator. The two pure helpers exported from
+`frontend/src/app/features/admin/general.pure.ts` make the wall-clock
+values behave as if they were already UTC, so the admin sees the same
+instant no matter where the browser is running:
+
+| Helper | Direction | Purpose |
+|------------------------------------------------|-------------------------------------|---------|
+| `toDatetimeLocal(iso: string): string` | ISO-8601 UTC → `YYYY-MM-DDTHH:mm` | Renders a backend UTC instant in the native picker using `getUTC*` getters. |
+| `toIsoUtc(local: string): string` | `YYYY-MM-DDTHH:mm[:ss[.fff]]` → ISO-8601 UTC | Parses the wall-clock components as **UTC** (not local time) via `Date.UTC(...)` and returns `toISOString()`. |
+
+The `toIsoUtc` helper explicitly does **not** call `new Date(local)`,
+which would interpret the components in the browser's local timezone
+and shift the round-tripped instant by the browser's UTC offset. Instead
+it regex-parses the year / month / day / hour / minute components (and
+optional seconds / milliseconds), constructs the instant with
+`new Date(Date.UTC(...))`, and returns `toISOString()`. As a result,
+`toIsoUtc(toDatetimeLocal(iso)) === iso` for any minute-precision
+`datetime-local` value the picker can produce, regardless of the
+browser's `Intl.DateTimeFormat().resolvedOptions().timeZone` value.
+
+The helper preserves its existing fallback contract:
+
+| Input | Output |
+|------------------------------------|---------------------------------|
+| `''` (empty) | `''` |
+| Non-matching `YYYY-MM-DDTHH:mm…` | Returns the input verbatim so the reactive-form `invalidDatetime` validator still fires. |
+| Matching `YYYY-MM-DDTHH:mm` | `:00.000Z` |
+| Matching `YYYY-MM-DDTHH:mm:ss` | `.000Z` |
+| Matching `YYYY-MM-DDTHH:mm:ss.fff` | `Z` (fraction padded to ms) |
+
+`AdminGeneralComponent` calls `toIsoUtc` on both `eventStartUtc` and
+`eventEndUtc` before issuing `PUT /api/v1/admin/general/settings`, so
+correcting the shared helper fixes start and end submissions without
+any further component, form, or backend rewiring. The backend zod
+schema (`GeneralSettingsSchema` in
+`backend/src/modules/admin/dto/general.dto.ts`) accepts the resulting
+timezone-aware ISO-8601 strings unchanged and enforces
+`eventEndUtc > eventStartUtc` via `superRefine`.
+
+The regression for this fix lives in
+`tests/frontend/admin-general-pure.spec.ts` under the `datetime helpers`
+suite, with cases that lock in:
+
+* `toIsoUtc('2027-03-15T08:30')` → `'2027-03-15T08:30:00.000Z'`
+ (would previously have returned `'2027-03-15T15:30:00.000Z'` from a
+ PDT browser).
+* `toIsoUtc('2027-03-15T08:30:15')` → `'2027-03-15T08:30:15.000Z'`.
+* `toIsoUtc('2027-03-15T08:30:15.123')` → `'2027-03-15T08:30:15.123Z'`.
+* `toIsoUtc(toDatetimeLocal('2027-03-15T08:30:00.000Z'))` round-trips
+ back to the original ISO string.
+
# Visual elements
| Element | Selector |
diff --git a/docs/index.md b/docs/index.md
index ac6c30f..b0bfccd 100644
--- a/docs/index.md
+++ b/docs/index.md
@@ -11,7 +11,7 @@ scoreboard, an event window with a public countdown, theming, and admin
controls.
The docs below are organized by purpose so agents can pull just the slice
-they need. Last regenerated 2026-07-22T14:50:25Z.
+they need. Last regenerated 2026-07-22T15:09:39Z.
# Architecture