Compare commits

...

4 Commits

Author SHA1 Message Date
b0txec e408fad26c Rewrite the root README as a short, non-technical project overview
CI / backend (push) Successful in 1m3s
CI / frontend (push) Successful in 42s
The old file was a confusing mix of eras: an accurate current
Windows/Rocky/VPS environment section sitting alongside stale leftovers
from before this project's current setup existed -- a wrong port (9090,
staging is actually 9190), old docker-compose v1 syntax, a podman-compose
three-terminal dev loop nobody uses, an nginx config for laikazinas.lsm.lv
(the real current LSM production site, unrelated to and confusable with
this project's actual temporary test domain), Fly.io commands from a
hosting platform no longer in use, and an example command that happens to
run one of the files this session's security review flagged as a
mislabeled test script capable of touching a live database.

Replaced with a short landing page: what the tool does, how it's built,
current status (Rocky-only development now that Windows is retired from
the workflow, laikapstak.li as the temporary test environment ahead of
eventually replacing the live laikazinas.lsm.lv service), and a security
summary -- all pointing to docs/ for actual technical depth rather than
duplicating half-true instructions. Updated docs/README.md's own
description of the root README to match.
2026-08-25 19:14:48 +03:00
b0txec cb7e9aa06b Record the Faktiskā blank-temperature fix and correct a runbook bug
CI / backend (push) Successful in 1m3s
CI / frontend (push) Successful in 40s
Documents release a6b2b84 (the queryLatestTemperatures fix and the user's
CSS polish pass) in the changelog and README status line. Also fixes a real
bug in VPS_RELEASE_RUNBOOK.md's own checksum step, found by actually running
it for this release: it generated the .sha256 file against a full local
path rather than a bare filename, which fails verification on the VPS since
that path doesn't exist there.
2026-08-25 17:19:17 +03:00
b0txec a6b2b8462c Fix Faktiskā showing blank temperatures right after each new hour starts
CI / backend (push) Successful in 1m37s
CI / frontend (push) Successful in 48s
queryLatestTemperatures picked the single most-recent row per city with no
regard for whether that row's tempAvg was actually populated yet. The
open-data source publishes fields on different schedules within the same
hour (snow lands well before the hourly temperature aggregate), so the
freshest row for a city routinely has a real dateTime but a still-null
tempAvg while the previous hour's row has good data. Confirmed live on both
Rocky and the VPS: every city's newest row had tempAvg null (but a real
snowAvg=0), one hour after the actual last real reading.

Fixed by requiring tempAvg IS NOT NULL in the WHERE clause, so DISTINCT ON's
"most recent" pick skips a fresh partial row and finds the newest row that
actually has a value. Checked before changing: this is the only caller of
queryLatestTemperatures (Faktiskā's fixed-station lookup); a city with zero
real temperature history ever now returns no row instead of a null-valued
one, which the frontend already treats identically to a present-but-null
observation (falls through to the same "Nav datu" missing state either
way) -- confirmed via MapGraphics.tsx before shipping, not assumed.
2026-08-25 17:13:20 +03:00
b0txec 829319edf8 Polish hover/focus states on inputs and checkboxes, drop a stray font override
- Add a .secondary button variant and hover/focus states (border-color,
  focus ring) to text/number/date/select inputs, matching the existing
  checkbox and button interaction language.
- Restyle Brīdinājumi's warning checkbox to match the same custom-checkbox
  look already used elsewhere (border-based, hover, focus-visible outline)
  instead of its own older shadow-based style.
- Remove a hardcoded Inter font-family on Faktiskā's reset-temperature
  button so it inherits the app's default Monda font like everything else.
2026-08-25 17:13:11 +03:00
8 changed files with 69 additions and 76 deletions
+40 -66
View File
@@ -1,77 +1,51 @@
# Weather Tool
# WeatherTool
Tool for downloading historical and real-time data from weather stations in Latvia. It aggregates and displays data for different time periods, cities, and weather parameters.
WeatherTool prepares the weather graphics used on air: current temperatures,
water temperatures, and severe-weather warnings, rendered as fixed-size PNG
exports ready to drop into a broadcast. It's a large update to the tool
currently running in production at `laikazinas.lsm.lv`.
## Setup
Rename `.env-sample` to `.env` and fill in credentials
## What it does
## Environment boundary
- Pulls real observation data from LVĢMC (Latvia's national weather
service) — current station temperatures, water temperatures, and active
warnings — from their public open-data feed.
- Lets an operator review and, where needed, correct individual values
before export, without touching the underlying data.
- Renders that data onto fixed newsroom map templates and exports
broadcast-ready PNGs at the station's required resolutions.
- Also includes analytical tools (historical station comparisons, a full
data archive) for exploring the underlying weather data beyond what goes
on air.
- **Windows:** source editing, review, and Git operations only. Do not compile, build, run containers, install project dependencies, or test the application here.
- **Rocky Linux:** the only development runtime; compile, build, run, seed, and test WeatherTool here.
- **Ubuntu VPS:** deployment target only. It receives the reviewed release artifact built and verified on Rocky; it is not a development or build host.
## How it's built
## Run
```
docker-compose up --build --no-cache --force-recreate
```
A Scala backend (cats-effect, http4s, PostgreSQL) ingests and serves the
data; a SolidJS frontend is where operators build the graphics. Both are
described in full under [`docs/`](docs/README.md).
## Web
[http://0.0.0.0:9090/](http://0.0.0.0:9090/)
## Status
## nginx proxy config
```
server {
listen 80;
server_name laikazinas.lsm.lv;
Actively in development. All development, testing, and staging currently
happens on a single Rocky Linux machine; a temporary public test
environment for the new version is at `laikapstak.li`, gated behind
authentication, separate from the live `laikazinas.lsm.lv` service it will
eventually replace. Continuous integration runs automatically on every
push via Gitea Actions.
location / {
proxy_pass http://localhost:9090; # Forward requests to the Scala app
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_cache_bypass $http_upgrade;
}
}
```
## Security
## Rocky development runtime
```
// 1st terminal
podman-compose up postgres
The application has been through an independent security review, with
findings (including a critical one) found and fixed — see
[`docs/UPDATE_ROADMAP.md`](docs/UPDATE_ROADMAP.md) for the full history.
The public test environment sits behind Cloudflare, an authentication
gate, and a private database with no public port. Deployments are manual,
checksummed, and smoke-tested before going live — see
[`docs/VPS_RELEASE_RUNBOOK.md`](docs/VPS_RELEASE_RUNBOOK.md).
// 2nd terminal
sbt run
## Getting started
// 3rd terminal
cd web/
npm run dev
```
Run one file
```sbt "runMain grib.GribParserTest"```
## Backend Tech:
- scala: cats-effects, http4s, fs2, circe, scalatest
- postgres
## Frontend Tech:
- SolidJS
- Vite
## Fly commands
```
// suspend instance
fly scale count 0
```
```
// check display
fly ssh console
df -h
```
```
fly scale memory 512
```
See [`docs/README.md`](docs/README.md) for current status, architecture,
and the full documentation set, and [`docs/DEVELOPMENT_AND_STAGING.md`](docs/DEVELOPMENT_AND_STAGING.md)
for exact build, run, and deployment commands. For local setup, copy
`.env-sample` to `.env` and fill in real values.
+2 -2
View File
@@ -1,6 +1,6 @@
# WeatherTool project documentation
This directory contains the working documentation for the WeatherTool modernization effort. The repository-root `README.md` is preserved as the original project overview; these documents describe the reviewed code, current staging environment, and changes being developed.
This directory contains the working documentation for the WeatherTool modernization effort. The repository-root `README.md` is a short, non-technical overview for anyone landing on the repo (purpose, status, security posture); these documents describe the reviewed code, current staging environment, and changes being developed in full technical depth.
## Current status
@@ -18,7 +18,7 @@ This directory contains the working documentation for the WeatherTool modernizat
- Confirmed local Monda Regular/Bold files provide interface and generated-graphic typography; weather symbols use normalized transparent image assets.
- Release `b0b58d2` (`weathertool:b0b58d2f1e0ed47ca13795b64595386ec2f0e0c7`) replaced the hardcoded per-route static-file list with a general SPA fallback — a real, user-reported bug where refreshing `/faktiska` or `/udens-temperatura` 404ed instead of loading the app (those two routes were never added to the old list). A missing `/assets` file still 404s properly rather than silently serving HTML.
- An independent fresh-eyes security review (requested against the local Rocky version only) found a live-verified CRITICAL path-traversal vulnerability plus several HIGH/MEDIUM findings — see the roadmap changelog for `6b9c7cf`/`8c45d8d` for the full list. All are fixed and deployed. Rocky's `POSTGRES_PASSWORD` was rotated afterward since the old value had been exposed into the review's own output via `/proc/self/environ`. `LVGMC_PASSWORD` rotation is not yet done — that needs the user's own action via the LVGMC contact. A follow-up independent review then caught that the FTP auth-bypass fix was incomplete (a sibling route, `/api/show/lvgmc-forecast`, had the same live-FTP-trigger issue but had only gotten the traversal fix); fixed as `9bab93d`. That same review flagged several open architectural/testability findings (the `parMapN` scheduler crash-loop pattern, per-request `System.gc()`, an unenforced field-routing invariant, no JDBC connection pooling, and several files under `src/main/scala` that look like tests but aren't) — not yet acted on, tracked for a future session.
- Release `001b014` is deployed as immutable image `weathertool:001b014932a1c5e28080dd057613f8bb807a3b81`; release `b0b58d2` remains the immediate application rollback. Carries everything since the last VPS push: the full security-review fix set above, the Gitea Actions CI setup, and the frontend polish/click-to-edit/Valmiera work described elsewhere on this page.
- Release `a6b2b84` is deployed as immutable image `weathertool:a6b2b8462c9adf1647ebddf92edb0e41d345d6e7`; release `001b014` remains the immediate application rollback. Fixes a real bug reported live by the user: Faktiskā showed blank temperatures for a stretch after every new hour started, because the "latest observation" query picked whichever row was newest regardless of whether its temperature field had actually landed yet (the open-data source publishes some fields, like snow, well before the hourly temperature aggregate). Isolated to Faktiskā — Kartes and Ūdens use different query shapes unaffected by the same timing gap. Also carries a small hover/focus-state and checkbox-styling polish pass. See the roadmap changelog for `a6b2b84`/`829319e`.
- Gitea Actions CI is live: a repository-scoped runner on Rocky (isolated behind its own Docker-in-Docker daemon, same pattern as HOP's Forgejo runner) runs `sbt test` plus the frontend typecheck/build/audit routine on every push/PR to `codex/staging-baseline`. See [Continuous integration](CONTINUOUS_INTEGRATION.md). CI-only for now — it does not deploy anywhere or touch any real credential.
- FTP (`ENABLE_LVGMC_FTP_JOBS`) is currently `false` on both Rocky and the VPS — re-testing the morning after enabling it turned up a second, unexplained failure (Rocky alone, VPS confirmed off, ~4 minutes after a successful manual test) that doesn't fit the original two-machine-collision theory. Decided to stop self-testing via trial and error and wait for the user to ask the LVGMC contact directly about the `ltv` account's connection/rate policy, rather than risk repeatedly tripping an unknown limit. See the roadmap Phase 7 for the full diagnosis timeline.
- The isolated VPS UAT stack is running and healthy: WeatherTool is bound to `127.0.0.1:8002`, Authelia to `127.0.0.1:9091`, and PostgreSQL has no host port. Public access is routed through Cloudflare, Nginx, and Authelia.
+2
View File
@@ -457,5 +457,7 @@ Record completed work here by date and commit after the Git workflow is establis
| 2026-08-25 | `e1ce153` | Give Ūdens's resolution-choice buttons the same visual language as the temperature cards next to them (border, radius, soft glass background) instead of the app's generic flat button style, after measuring (via Playwright bounding boxes, not eyeballing) that the perceived "buttons sit higher" was a visual-weight mismatch, not an actual coordinate offset — the tops were already pixel-identical | Yes — Rocky rebuild, headless-browser screenshot confirmation |
| 2026-08-25 | `07b16ac` | Source Valmiera's Faktiskā temperature from Priekuļi as a temporary substitute, since LVĢMC's open-data station list has no station in or near Valmiera at all. Priekuļi chosen by measuring against the app's own calibrated map coordinates (~56px away vs. the next-closest candidate's ~81px), not a guess — same pattern as Stende/Zīlāni substituting for Talsi/Jēkabpils. Implemented client-side in Faktiskā's fetch only; Valmiera's marker position and on-map label are untouched. Also fixes a second symptom for free: the weather-symbol picker only lists cities with a value, so Valmiera never appeared there either. Provisional pending direct confirmation from LVĢMC | Yes — typecheck clean, production build succeeds, headless-browser checks confirm Valmiera's field shows Priekuļi's real live value (fresh, not flagged manual/stale) and now appears in the weather-symbol exceptions list (13 cities, was 12) |
| 2026-08-25 | `4c40c24` | Add click-to-edit temperature badges directly on the Faktiskā map, as a fast path for the common "one or two values need a tweak" case instead of always opening the 13-field "Rādīt stacijas" panel. Mirrors Brīdinājumi's existing canvas-coordinate pattern exactly (`getBoundingClientRect` scaling) rather than inventing a new one, hit-testing against Faktiskā's already-fixed per-resolution marker positions. The floating input is a plain DOM element positioned over the canvas, never drawn onto it, so it stays preview-only like Brīdinājumi's drag handles. Reuses the existing `updateValue` function via a new `onEditValue` callback prop, so the "Rādīt stacijas" panel's override tracking/reset/manual-count badge all stay in sync automatically. Kept the panel rather than removing it after this shipped — it's the only place that surfaces missing/stale data and offers a reset, neither of which click-to-edit does | Yes — end-to-end Playwright test: hover shows a pointer cursor, click shows a correctly positioned input prefilled with the real value, Enter commits and the canvas redraws, the change appears in the panel as a manual override, Escape cancels without committing, works correctly at both export resolutions, no console errors, Kartes (a different `MapView` mode) unaffected |
| 2026-08-25 | `829319e` | Polish hover/focus states on inputs and checkboxes (matching the checkbox/button interaction language already established elsewhere), restyle Brīdinājumi's warning checkbox to match, and drop a stray hardcoded Inter font override on Faktiskā's reset-temperature button | Made directly by the user, included in the next release |
| 2026-08-25 | `a6b2b84` | Fix Faktiskā showing blank temperatures for a stretch after every new hour starts, reported live by the user on both Rocky and the VPS. Root cause: `queryLatestTemperatures` picked the single most-recent row per city with no regard for whether that row's `tempAvg` was actually populated — the open-data source publishes fields on different schedules within an hour (snow arrives well before the hourly temperature aggregate), so the freshest row routinely has a real timestamp but a still-null `tempAvg` while the previous hour's row has good data. Confirmed live on both Rocky and the VPS before fixing: every city's newest row had `tempAvg` null but a real `snowAvg=0`. Fixed by requiring `tempAvg IS NOT NULL` in the query, so it finds the newest row that actually has a value instead of a fresher partial one. Isolated to Faktiskā — Kartes aggregates over a range (one partial hour doesn't blank the result) and Ūdens is a separate data source entirely, which is why only Faktiskā showed the symptom | Yes — confirmed the root cause directly (upstream open-data API check, full-row DB inspection on both Rocky and VPS) before writing the fix, not assumed; compiled; deployed to both Rocky and the VPS following `VPS_RELEASE_RUNBOOK.md` (which itself got a real bug fixed while running it for the first time — the checksum file recorded a full local path instead of a bare filename, failing verification on the VPS); confirmed all 13 Faktiskā stations show real `15:00` data on Rocky via headless browser, confirmed the same live on the VPS via curl, public HTTPS gate unchanged (302/401) |
| 2026-08-25 | `001b014` | Deploy everything accumulated since the last VPS push (`9bab93d`) as image `weathertool:001b014932a1c5e28080dd057613f8bb807a3b81`: the frontend padding/cleanup/Ūdens-layout/button-styling pass, the Valmiera→Priekuļi substitution, and click-to-edit on Faktiskā. The Gitea CI setup itself doesn't affect the deployed app. Release `b0b58d2` remains the rollback target | Yes — exact release image smoke-tested on Rocky against a throwaway local Postgres before transfer (traversal/injection payloads 404, gated FTP routes 503, `/faktiska` and `/udens-temperatura` direct hits 200); confirmed the built image's CSS bundle genuinely contains tonight's changes (not a stale cache hit) before shipping; checksum verified on both ends; `app` service recreated cleanly (healthy, schedulers registered, no errors in logs); loopback re-verification of the same security/routing checks plus confirmed Priekuļi has real live data server-side; public HTTPS unchanged (302/401). Browser-level verification of Valmiera/click-to-edit specifically relied on Rocky's already-passing headless-browser test of the identical code, since the VPS app port is loopback-only by design and not reachable for a direct browser check from this session |
| 2026-08-24 | `2c44888``b0b58d2` | Replace the hardcoded per-route static-file list (`/station`, `/cities`, `/latvia`, `/database`, `/harmonie`, `/lvgmc-forecast`, `/bridinajumi`) with a general SPA fallback: real files serve as-is, anything else falls back to `index.html` so the SolidJS router handles it client-side — fixes a real, user-reported bug where a direct hit (e.g. a browser refresh) on `/faktiska` or `/udens-temperatura` 404ed instead of loading the app, since those two routes were never added to the old list. A missing file under `/assets` specifically still 404s properly rather than silently serving HTML, so a stale tab after a future deploy gets a clean error instead of a confusing JS parse failure | Yes — first attempt silently no-op'd because the fix was built into a `git archive HEAD` image before being committed (classic mistake, caught immediately by re-testing and finding identical old behavior); after committing, verified via curl on all previously-working routes, both previously-broken routes, a missing asset (404), a real asset (200), and `/api` (200), then a full headless-browser render check (zero console errors, real data, correct nav state) on a genuine direct hit — not just HTTP status codes; deployed to VPS in release `b0b58d2`, verified the same way through both the loopback port and the public domain (Authelia gate still correctly redirects unauthenticated requests) |
+9 -4
View File
@@ -74,15 +74,20 @@ with the new commit.
```bash
mkdir -p /tmp/weathertool-release
OUT=/tmp/weathertool-release/weathertool-$SHA.tar.gz
docker save weathertool:$SHA | gzip > "$OUT"
sha256sum "$OUT" | tee "$OUT.sha256"
cd /tmp/weathertool-release
docker save weathertool:$SHA | gzip > weathertool-$SHA.tar.gz
sha256sum weathertool-$SHA.tar.gz | tee weathertool-$SHA.tar.gz.sha256
```
Generate the checksum from a bare filename (after `cd`-ing into the
directory), not a full path — `sha256sum -c` on the VPS checks the exact
path string recorded in the `.sha256` file, and a local absolute path won't
exist there. (Found by running this step for real, not a hypothetical.)
## 4. Transfer to the VPS and verify
```bash
scp "$OUT" "$OUT.sha256" vps:/tmp/
scp weathertool-$SHA.tar.gz weathertool-$SHA.tar.gz.sha256 vps:/tmp/
ssh vps "cd /tmp && sha256sum -c weathertool-$SHA.tar.gz.sha256"
# must print: weathertool-<SHA>.tar.gz: OK
+7
View File
@@ -29,10 +29,17 @@ object PostgresService {
class PostgresService(transactor: Transactor[IO], log: Logger[IO]) {
def queryLatestTemperatures(cities: NonEmptyList[String]): IO[List[(String, LocalDateTime, Option[Double])]] = {
// The open-data source publishes fields on different schedules within
// the same hour (e.g. snow arrives well before the hourly temperature
// aggregate), so the single newest row for a city can have a real
// dateTime but a still-null tempAvg. Skip straight to DISTINCT ON's
// "most recent" pick by requiring tempAvg itself to be present, so a
// fresh partial row never hides an older row that actually has data.
val query =
fr"SELECT DISTINCT ON (city) city, dateTime, tempAvg" ++
fr" FROM weather" ++
fr" WHERE " ++ Fragments.in(fr"city", cities) ++
fr" AND tempAvg IS NOT NULL" ++
fr" ORDER BY city, dateTime DESC"
query.query[(String, LocalDateTime, Option[Double])]
+4 -1
View File
@@ -49,10 +49,13 @@ thead {
}
.primary{background:var(--accent);color:#fff}.primary:hover{background:var(--accent-strong)}
.secondary{background:var(--surface);color:var(--accent-strong);border-color:var(--accent-soft)}.secondary:hover{background:var(--accent-pale);border-color:var(--accent)}
button,input[type=button]{transition:transform 260ms cubic-bezier(.23,1,.32,1),box-shadow 260ms,background-color 180ms,border-color 180ms;touch-action:manipulation;will-change:transform}button:not(:disabled):hover,input[type=button]:not(:disabled):hover{transform:translateY(-2px);box-shadow:0 8px 16px rgba(26,74,128,.16)}button:not(:disabled):active,input[type=button]:not(:disabled):active{transform:translateY(0);box-shadow:none}
input[type=checkbox]{--checkbox-size:18px;appearance:none;width:var(--checkbox-size);height:var(--checkbox-size);margin:0 6px 0 0;vertical-align:-3px;border:1px solid #aebed1;border-radius:5px;background:#fff;cursor:pointer;position:relative;transition:all .25s}input[type=checkbox]:hover{border-color:var(--accent)}input[type=checkbox]:checked{border-color:transparent;background:linear-gradient(145deg,#3388d7,#1763aa);box-shadow:0 0 0 3px rgba(36,119,197,.13)}input[type=checkbox]:checked:before{content:"";position:absolute;top:45%;left:50%;width:4px;height:8px;border-right:2px solid #fff;border-bottom:2px solid #fff;transform:translate(-50%,-50%) rotate(45deg) scale(1);animation:check-pop .22s cubic-bezier(.12,.4,.29,1.46)}input[type=checkbox]:focus-visible{outline:3px solid var(--accent-soft);outline-offset:2px}@keyframes check-pop{from{opacity:0;transform:translate(-50%,-50%) rotate(45deg) scale(0)}to{opacity:1;transform:translate(-50%,-50%) rotate(45deg) scale(1)}}
@media(prefers-reduced-motion:reduce){*,*:before,*:after{scroll-behavior:auto!important;animation-duration:.01ms!important;animation-iteration-count:1!important;transition-duration:.01ms!important}}
input[type=text],input[type=number],input[type=date],input[type=datetime-local],select{min-height:38px;padding:7px 10px;border:1px solid #cfd7e2;border-radius:8px;background:#fff;font-family:inherit}button:disabled,input:disabled{cursor:not-allowed;opacity:.48}.appContent{min-height:calc(100vh - 73px);padding-inline:clamp(16px,3vw,48px)}
input[type=text],input[type=number],input[type=date],input[type=datetime-local],select{min-height:38px;padding:7px 10px;border:1px solid #cfd7e2;border-radius:8px;background:#fff;font-family:inherit;transition:border-color 180ms,box-shadow 180ms}
input[type=text]:hover,input[type=number]:hover,input[type=date]:hover,input[type=datetime-local]:hover,select:hover{border-color:var(--accent-soft)}
input[type=text]:focus,input[type=number]:focus,input[type=date]:focus,input[type=datetime-local]:focus,select:focus{outline:none;border-color:var(--accent);box-shadow:0 0 0 3px rgba(36,119,197,.15)}button:disabled,input:disabled{cursor:not-allowed;opacity:.48}.appContent{min-height:calc(100vh - 73px);padding-inline:clamp(16px,3vw,48px)}
table td {
padding: 3px 7px;
+1 -1
View File
@@ -25,7 +25,7 @@
.temperatureField.missing{border-style:dashed}
.temperatureFieldHeading{grid-area:heading;min-width:0}.temperatureFieldHeading strong,.temperatureFieldHeading small{display:block}.temperatureFieldHeading small{margin-top:2px;color:var(--text-muted);font-size:10px}
.temperatureInput{grid-area:input;display:flex;align-items:center;gap:7px}.temperatureInput input{width:100%;min-width:0;font-size:19px;font-weight:700}.temperatureInput>span{color:var(--text-muted);font-weight:700}
.resetTemperature{grid-area:reset;align-self:start;min-height:0;padding:3px 5px;background:transparent;color:var(--accent-strong);font-family:Inter,sans-serif;font-size:10px}
.resetTemperature{grid-area:reset;align-self:start;min-height:0;padding:3px 5px;background:transparent;color:var(--accent-strong);font-size:10px}
.inlineNotice{margin:10px 0;padding:10px 12px;border-radius:9px;background:rgba(255,255,255,.62)}.inlineNotice.error{color:var(--danger)}
.faktiskaProduction{margin-top:20px}.faktiskaProduction>h2{font-size:22px}
.faktiskaResolutionChoices{display:flex;gap:8px;margin:0 0 14px}.faktiskaResolutionChoices button{min-width:150px}.faktiskaResolutionChoices button.active{border-color:var(--accent);background:var(--accent-pale);color:var(--accent-strong)}
+4 -2
View File
@@ -23,10 +23,12 @@
.warningCheckboxLabel{position:relative;display:inline-flex;flex:0 0 auto;width:22px;height:22px;cursor:pointer}
.warningCheckboxLabel .warningCheckboxInput{position:absolute;opacity:0;height:0;width:0;margin:0}
.warningCheckmark{position:absolute;inset:0;background-color:#eee;border-radius:5px;transition:background-color .3s;box-shadow:0 2px 5px rgba(0,0,0,.2)}
.warningCheckmark{position:absolute;inset:0;background:#fff;border:1px solid #aebed1;border-radius:5px;transition:all .25s}
.warningCheckmark:after{content:"";position:absolute;display:none;left:7px;top:3px;width:5px;height:10px;border:solid #fff;border-width:0 3px 3px 0;transform:rotate(45deg)}
.warningCheckboxInput:checked ~ .warningCheckmark{background-color:#2196F3;box-shadow:0 3px 7px rgba(33,150,243,.3)}
.warningCheckboxLabel:hover .warningCheckmark{border-color:var(--accent)}
.warningCheckboxInput:checked ~ .warningCheckmark{border-color:transparent;background:linear-gradient(145deg,#3388d7,#1763aa);box-shadow:0 0 0 3px rgba(36,119,197,.13)}
.warningCheckboxInput:checked ~ .warningCheckmark:after{display:block;animation:warningCheckAnim .2s forwards}
.warningCheckboxInput:focus-visible ~ .warningCheckmark{outline:3px solid var(--accent-soft);outline-offset:2px}
@keyframes warningCheckAnim{0%{height:0}100%{height:10px}}
.warningBackdrop{position:fixed;inset:0;z-index:1000;display:flex;align-items:center;justify-content:center;padding:24px;background:rgba(10,20,35,.55);backdrop-filter:blur(6px);animation:warningFadeIn 180ms ease}