Compare commits
6 Commits
c547cf2389
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 841046ba36 | |||
| fc9cc2a150 | |||
| 7dc58121f0 | |||
| 38a612ae71 | |||
| ee5d0d715a | |||
| 0e13274036 |
@@ -1 +1,2 @@
|
|||||||
*.exe
|
*.exe
|
||||||
|
dist/
|
||||||
|
|||||||
@@ -175,5 +175,24 @@
|
|||||||
"closed": false,
|
"closed": false,
|
||||||
"createdAt": "2026-06-10T16:01:51.4082662Z",
|
"createdAt": "2026-06-10T16:01:51.4082662Z",
|
||||||
"adminToken": "wBmkZL25zTkrzYgCQBjtGxWY"
|
"adminToken": "wBmkZL25zTkrzYgCQBjtGxWY"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "y4rE5zp9hW",
|
||||||
|
"title": "Ad",
|
||||||
|
"description": "awdwadw\n\nawdawdwa\nwadwda\n\n\nwadw",
|
||||||
|
"options": [
|
||||||
|
{
|
||||||
|
"id": "mkaa95Gr",
|
||||||
|
"date": "2026-06-18"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "pQbnHN7b",
|
||||||
|
"date": "2026-06-20"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"votes": [],
|
||||||
|
"closed": false,
|
||||||
|
"createdAt": "2026-06-10T16:39:18.0584144Z",
|
||||||
|
"adminToken": "hKD8vupZhQN6vY2BUbXYRwfj"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
# Deploying mediator
|
||||||
|
|
||||||
|
Target: Ubuntu 22.04 VPS, nginx in front, app in `/opt/mediator`.
|
||||||
|
|
||||||
|
The app runs as an unprivileged `mediator` system user, listens only on
|
||||||
|
`127.0.0.1:8080`, and nginx proxies the public domain to it. Polls live in
|
||||||
|
`/opt/mediator/data/polls.json` — that one file is the whole backup.
|
||||||
|
|
||||||
|
## One-time setup (on the server)
|
||||||
|
|
||||||
|
Copy the deploy files over and run the setup script as root:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
scp deploy/mediator.service deploy/mediator.nginx.conf deploy/setup-server.sh himalia:/tmp/
|
||||||
|
ssh himalia
|
||||||
|
cd /tmp
|
||||||
|
sudo ./setup-server.sh <your-ssh-user> <your-domain>
|
||||||
|
```
|
||||||
|
|
||||||
|
The script creates the `mediator` user, installs the systemd unit and nginx
|
||||||
|
site, and adds a sudoers rule so your user can `systemctl restart mediator`
|
||||||
|
without a password (that keeps deploys to a single password prompt).
|
||||||
|
|
||||||
|
Then get a certificate:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
sudo certbot --nginx -d <your-domain>
|
||||||
|
```
|
||||||
|
|
||||||
|
## Every deploy (from your machine)
|
||||||
|
|
||||||
|
```sh
|
||||||
|
./deploy/deploy.sh # or: ./deploy/deploy.sh otherhost
|
||||||
|
```
|
||||||
|
|
||||||
|
Cross-compiles a static linux/amd64 binary, streams it to the server over
|
||||||
|
one ssh connection (one password prompt), swaps it in atomically, restarts
|
||||||
|
the service, and prints `active` on success.
|
||||||
|
|
||||||
|
## Useful commands on the server
|
||||||
|
|
||||||
|
```sh
|
||||||
|
systemctl status mediator
|
||||||
|
journalctl -u mediator -f
|
||||||
|
cp /opt/mediator/data/polls.json ~/polls-backup.json # backup
|
||||||
|
```
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Build mediator for linux/amd64 and push it to the server.
|
||||||
|
# Run from your machine (Git Bash on Windows works):
|
||||||
|
#
|
||||||
|
# ./deploy/deploy.sh [host] default host: himalia
|
||||||
|
#
|
||||||
|
# Requires the one-time server setup (setup-server.sh) to have been run.
|
||||||
|
# Asks for the ssh password once; the binary is streamed over that same
|
||||||
|
# connection, swapped in atomically, and the service restarted.
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
HOST="${1:-himalia}"
|
||||||
|
APP_DIR=/opt/mediator
|
||||||
|
cd "$(dirname "$0")/.."
|
||||||
|
|
||||||
|
echo "Building linux/amd64 binary..."
|
||||||
|
mkdir -p dist
|
||||||
|
GOOS=linux GOARCH=amd64 CGO_ENABLED=0 \
|
||||||
|
go build -trimpath -ldflags "-s -w" -o dist/mediator-linux-amd64 .
|
||||||
|
|
||||||
|
echo "Uploading to $HOST and restarting mediator..."
|
||||||
|
ssh "$HOST" "set -e
|
||||||
|
cat > $APP_DIR/mediator.new
|
||||||
|
chmod 755 $APP_DIR/mediator.new
|
||||||
|
mv -f $APP_DIR/mediator.new $APP_DIR/mediator
|
||||||
|
sudo systemctl restart mediator
|
||||||
|
sleep 1
|
||||||
|
systemctl is-active mediator
|
||||||
|
" < dist/mediator-linux-amd64
|
||||||
|
|
||||||
|
echo "Deployed $(git rev-parse --short HEAD) to $HOST."
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
# nginx site for mediator. Installed by setup-server.sh to
|
||||||
|
# /etc/nginx/sites-available/mediator with the real domain substituted.
|
||||||
|
# For HTTPS run `certbot --nginx -d <domain>` afterwards; certbot rewrites
|
||||||
|
# this file to add the TLS server block and the HTTP->HTTPS redirect.
|
||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
listen [::]:80;
|
||||||
|
server_name mediator.example.com;
|
||||||
|
|
||||||
|
location / {
|
||||||
|
proxy_pass http://127.0.0.1:8080;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=mediator - date polls for friend groups
|
||||||
|
After=network-online.target
|
||||||
|
Wants=network-online.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
User=mediator
|
||||||
|
Group=mediator
|
||||||
|
ExecStart=/opt/mediator/mediator -addr 127.0.0.1:8080 -data /opt/mediator/data
|
||||||
|
Restart=on-failure
|
||||||
|
RestartSec=2
|
||||||
|
|
||||||
|
# Sandboxing: the service only needs to read its binary and write its data dir.
|
||||||
|
NoNewPrivileges=true
|
||||||
|
ProtectSystem=strict
|
||||||
|
ReadWritePaths=/opt/mediator/data
|
||||||
|
ProtectHome=true
|
||||||
|
PrivateTmp=true
|
||||||
|
PrivateDevices=true
|
||||||
|
ProtectKernelTunables=true
|
||||||
|
ProtectControlGroups=true
|
||||||
|
RestrictSUIDSGID=true
|
||||||
|
LockPersonality=true
|
||||||
|
MemoryDenyWriteExecute=true
|
||||||
|
RestrictAddressFamilies=AF_INET AF_INET6
|
||||||
|
CapabilityBoundingSet=
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# One-time server setup for mediator on Ubuntu. Run as root ON THE SERVER,
|
||||||
|
# from a directory containing mediator.service and mediator.nginx.conf:
|
||||||
|
#
|
||||||
|
# sudo ./setup-server.sh <deploy-user> <domain>
|
||||||
|
#
|
||||||
|
# <deploy-user> the account you ssh in as; it gets write access to
|
||||||
|
# /opt/mediator and passwordless `systemctl restart mediator`
|
||||||
|
# <domain> the public hostname nginx should answer on
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
DEPLOY_USER="${1:?usage: setup-server.sh <deploy-user> <domain>}"
|
||||||
|
DOMAIN="${2:?usage: setup-server.sh <deploy-user> <domain>}"
|
||||||
|
APP_DIR=/opt/mediator
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||||
|
|
||||||
|
id -u "$DEPLOY_USER" >/dev/null
|
||||||
|
|
||||||
|
# Unprivileged system user the service runs as.
|
||||||
|
id -u mediator >/dev/null 2>&1 ||
|
||||||
|
useradd --system --home "$APP_DIR" --shell /usr/sbin/nologin mediator
|
||||||
|
|
||||||
|
# Deploy user owns the app dir (to replace the binary); the service user
|
||||||
|
# owns only the data dir (the single thing it writes).
|
||||||
|
mkdir -p "$APP_DIR/data"
|
||||||
|
chown "$DEPLOY_USER" "$APP_DIR"
|
||||||
|
chmod 755 "$APP_DIR"
|
||||||
|
chown mediator:mediator "$APP_DIR/data"
|
||||||
|
chmod 750 "$APP_DIR/data"
|
||||||
|
|
||||||
|
install -m 644 "$SCRIPT_DIR/mediator.service" /etc/systemd/system/mediator.service
|
||||||
|
|
||||||
|
sed "s/mediator\.example\.com/$DOMAIN/" "$SCRIPT_DIR/mediator.nginx.conf" \
|
||||||
|
> /etc/nginx/sites-available/mediator
|
||||||
|
ln -sf /etc/nginx/sites-available/mediator /etc/nginx/sites-enabled/mediator
|
||||||
|
|
||||||
|
# Let the deploy user restart the service without a sudo password,
|
||||||
|
# so deploy.sh needs exactly one (ssh) password prompt.
|
||||||
|
printf '%s ALL=(root) NOPASSWD: /usr/bin/systemctl restart mediator\n' "$DEPLOY_USER" \
|
||||||
|
> /etc/sudoers.d/mediator-deploy
|
||||||
|
chmod 440 /etc/sudoers.d/mediator-deploy
|
||||||
|
|
||||||
|
systemctl daemon-reload
|
||||||
|
systemctl enable mediator
|
||||||
|
nginx -t
|
||||||
|
systemctl reload nginx
|
||||||
|
|
||||||
|
echo "Setup done. Now push a binary from your machine: ./deploy/deploy.sh"
|
||||||
|
echo "For HTTPS: certbot --nginx -d $DOMAIN"
|
||||||
+11
-6
@@ -3,7 +3,8 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
<title>mediator — new poll</title>
|
<title>mediator - new poll</title>
|
||||||
|
<link rel="icon" type="image/svg+xml" href="/static/logo.svg">
|
||||||
<link rel="preload" href="/static/fonts/IosevkaEtoile.woff2" as="font" type="font/woff2" crossorigin>
|
<link rel="preload" href="/static/fonts/IosevkaEtoile.woff2" as="font" type="font/woff2" crossorigin>
|
||||||
<link rel="preload" href="/static/fonts/IosevkaSlab.woff2" as="font" type="font/woff2" crossorigin>
|
<link rel="preload" href="/static/fonts/IosevkaSlab.woff2" as="font" type="font/woff2" crossorigin>
|
||||||
<link rel="stylesheet" href="/static/style.css">
|
<link rel="stylesheet" href="/static/style.css">
|
||||||
@@ -11,8 +12,8 @@
|
|||||||
<body>
|
<body>
|
||||||
<div class="wrap">
|
<div class="wrap">
|
||||||
<header class="site">
|
<header class="site">
|
||||||
<a class="logo" href="/"><span class="dot"></span>mediator</a>
|
<a class="logo" href="/"><svg class="dot" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.92" aria-hidden="true"><rect x="2.5" y="2.5" width="19" height="19"/><path d="M9.1 2.5 V15.1 M2.5 15.1 H21.5 M9.1 9 H21.5 M15 15.1 V21.5"/></svg>mediator</a>
|
||||||
<span class="tagline">one calendar for the whole crew</span>
|
<span class="tagline">how hard can it be to get 4 people around a table?</span>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<div id="create-view">
|
<div id="create-view">
|
||||||
@@ -22,11 +23,11 @@
|
|||||||
<div class="card">
|
<div class="card">
|
||||||
<label class="field">
|
<label class="field">
|
||||||
<span>What's the plan?</span>
|
<span>What's the plan?</span>
|
||||||
<input type="text" id="title" maxlength="120" placeholder="Pizza & board games at Lena's" autocomplete="off">
|
<input type="text" id="title" maxlength="120" autocomplete="off">
|
||||||
</label>
|
</label>
|
||||||
<label class="field">
|
<label class="field">
|
||||||
<span>Details <span class="hint">(optional)</span></span>
|
<span>Details (optional)</span>
|
||||||
<textarea id="description" maxlength="600" placeholder="Bring a game. We'll order around 7."></textarea>
|
<textarea id="description" maxlength="600"></textarea>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -74,6 +75,10 @@
|
|||||||
<a class="btn btn-ghost" href="/">Make another</a>
|
<a class="btn btn-ghost" href="/">Make another</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<footer class="site">
|
||||||
|
made by <span class="author">luxick</span> · <a href="https://git.luxick.de/luxick/mediator">source code</a>
|
||||||
|
</footer>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="/static/calendar.js"></script>
|
<script src="/static/calendar.js"></script>
|
||||||
|
|||||||
@@ -0,0 +1,4 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#c48401" stroke-width="1.92">
|
||||||
|
<rect x="2.5" y="2.5" width="19" height="19"/>
|
||||||
|
<path d="M9.1 2.5 V15.1 M2.5 15.1 H21.5 M9.1 9 H21.5 M15 15.1 V21.5"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 262 B |
+9
-4
@@ -3,7 +3,8 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
<title>mediator — poll</title>
|
<title>mediator - poll</title>
|
||||||
|
<link rel="icon" type="image/svg+xml" href="/static/logo.svg">
|
||||||
<link rel="preload" href="/static/fonts/IosevkaEtoile.woff2" as="font" type="font/woff2" crossorigin>
|
<link rel="preload" href="/static/fonts/IosevkaEtoile.woff2" as="font" type="font/woff2" crossorigin>
|
||||||
<link rel="preload" href="/static/fonts/IosevkaSlab.woff2" as="font" type="font/woff2" crossorigin>
|
<link rel="preload" href="/static/fonts/IosevkaSlab.woff2" as="font" type="font/woff2" crossorigin>
|
||||||
<link rel="stylesheet" href="/static/style.css">
|
<link rel="stylesheet" href="/static/style.css">
|
||||||
@@ -11,8 +12,8 @@
|
|||||||
<body>
|
<body>
|
||||||
<div class="wrap">
|
<div class="wrap">
|
||||||
<header class="site">
|
<header class="site">
|
||||||
<a class="logo" href="/"><span class="dot"></span>mediator</a>
|
<a class="logo" href="/"><svg class="dot" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.92" aria-hidden="true"><rect x="2.5" y="2.5" width="19" height="19"/><path d="M9.1 2.5 V15.1 M2.5 15.1 H21.5 M9.1 9 H21.5 M15 15.1 V21.5"/></svg>mediator</a>
|
||||||
<span class="tagline">one calendar for the whole crew</span>
|
<span class="tagline">how hard can it be to get 4 people around a table?</span>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<div id="not-found" hidden>
|
<div id="not-found" hidden>
|
||||||
@@ -34,7 +35,7 @@
|
|||||||
<div class="card" id="vote-card">
|
<div class="card" id="vote-card">
|
||||||
<label class="field">
|
<label class="field">
|
||||||
<span>Your name</span>
|
<span>Your name</span>
|
||||||
<input type="text" id="voter-name" maxlength="60" placeholder="So the group knows who answered" autocomplete="name">
|
<input type="text" id="voter-name" maxlength="60" autocomplete="name">
|
||||||
</label>
|
</label>
|
||||||
<div class="actions">
|
<div class="actions">
|
||||||
<button class="btn btn-primary" id="vote-btn">Send my answer</button>
|
<button class="btn btn-primary" id="vote-btn">Send my answer</button>
|
||||||
@@ -59,6 +60,10 @@
|
|||||||
<p class="error" id="admin-error" role="alert"></p>
|
<p class="error" id="admin-error" role="alert"></p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<footer class="site">
|
||||||
|
made by <span class="author">luxick</span> · <a href="https://git.luxick.de/luxick/mediator">source code</a>
|
||||||
|
</footer>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="/static/calendar.js"></script>
|
<script src="/static/calendar.js"></script>
|
||||||
|
|||||||
@@ -146,6 +146,27 @@ function renderResults() {
|
|||||||
row.append(when, who, count);
|
row.append(when, who, count);
|
||||||
el.appendChild(row);
|
el.appendChild(row);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const noneWork = poll.votes.filter((v) => v.optionIds.length === 0).map((v) => v.name);
|
||||||
|
if (noneWork.length > 0) {
|
||||||
|
const row = document.createElement("div");
|
||||||
|
row.className = "result-row none-row";
|
||||||
|
|
||||||
|
const when = document.createElement("span");
|
||||||
|
when.className = "when";
|
||||||
|
when.textContent = "None at all";
|
||||||
|
|
||||||
|
const who = document.createElement("span");
|
||||||
|
who.className = "who";
|
||||||
|
who.textContent = noneWork.join(", ");
|
||||||
|
|
||||||
|
const count = document.createElement("span");
|
||||||
|
count.className = "count";
|
||||||
|
count.textContent = noneWork.length;
|
||||||
|
|
||||||
|
row.append(when, who, count);
|
||||||
|
el.appendChild(row);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$("vote-btn").addEventListener("click", async () => {
|
$("vote-btn").addEventListener("click", async () => {
|
||||||
|
|||||||
+42
-5
@@ -82,10 +82,10 @@ header.site {
|
|||||||
|
|
||||||
.logo .dot {
|
.logo .dot {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
width: 0.7em;
|
width: 0.9em;
|
||||||
height: 0.7em;
|
height: 0.9em;
|
||||||
background: var(--primary);
|
color: var(--secondary);
|
||||||
border: 1px solid var(--secondary);
|
vertical-align: -0.1em;
|
||||||
margin-right: 0.4em;
|
margin-right: 0.4em;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -94,6 +94,18 @@ header.site {
|
|||||||
font-size: var(--font-sm);
|
font-size: var(--font-sm);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* === Footer === */
|
||||||
|
footer.site {
|
||||||
|
margin-top: var(--space-5);
|
||||||
|
padding: var(--space-3) 0;
|
||||||
|
border-top: var(--border-dashed);
|
||||||
|
color: var(--text-muted);
|
||||||
|
font-size: var(--font-sm);
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
footer.site .author { color: var(--secondary); }
|
||||||
|
|
||||||
/* === Typography === */
|
/* === Typography === */
|
||||||
h1 {
|
h1 {
|
||||||
font-size: 1.75rem;
|
font-size: 1.75rem;
|
||||||
@@ -106,6 +118,23 @@ h1 {
|
|||||||
|
|
||||||
.sub { color: var(--text-muted); margin: 0 0 var(--space-5); max-width: 60ch; }
|
.sub { color: var(--text-muted); margin: 0 0 var(--space-5); max-width: 60ch; }
|
||||||
|
|
||||||
|
#poll-desc { white-space: pre-line; }
|
||||||
|
|
||||||
|
#vote-hint {
|
||||||
|
font-size: var(--font-sm);
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
#vote-hint::before {
|
||||||
|
content: "> ";
|
||||||
|
color: var(--secondary);
|
||||||
|
font-style: normal;
|
||||||
|
}
|
||||||
|
/* Dashed rule between description and hint — only when a description is shown. */
|
||||||
|
#poll-desc:not([hidden]) + #vote-hint {
|
||||||
|
border-top: var(--border-dashed);
|
||||||
|
padding-top: var(--space-4);
|
||||||
|
}
|
||||||
|
|
||||||
.hint { font-weight: normal; color: var(--text-muted); font-size: var(--font-sm); }
|
.hint { font-weight: normal; color: var(--text-muted); font-size: var(--font-sm); }
|
||||||
|
|
||||||
.note { font-size: var(--font-sm); color: var(--text-muted); }
|
.note { font-size: var(--font-sm); color: var(--text-muted); }
|
||||||
@@ -336,7 +365,7 @@ textarea { resize: vertical; min-height: 4rem; }
|
|||||||
bottom: 3px;
|
bottom: 3px;
|
||||||
left: 50%;
|
left: 50%;
|
||||||
transform: translateX(-50%);
|
transform: translateX(-50%);
|
||||||
font-size: 0.6rem;
|
font-size: 0.8rem;
|
||||||
line-height: 1;
|
line-height: 1;
|
||||||
color: var(--link);
|
color: var(--link);
|
||||||
}
|
}
|
||||||
@@ -421,6 +450,9 @@ textarea { resize: vertical; min-height: 4rem; }
|
|||||||
font-variant-numeric: tabular-nums;
|
font-variant-numeric: tabular-nums;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.result-row.none-row { border-top: var(--border); margin-top: var(--space-2); }
|
||||||
|
.result-row.none-row .when, .result-row.none-row .count { color: var(--danger); }
|
||||||
|
|
||||||
.result-row.best .when, .result-row.best .count { color: var(--link); }
|
.result-row.best .when, .result-row.best .count { color: var(--link); }
|
||||||
.result-row.best:hover .when, .result-row.best:hover .count { color: var(--link); }
|
.result-row.best:hover .when, .result-row.best:hover .count { color: var(--link); }
|
||||||
|
|
||||||
@@ -453,6 +485,11 @@ textarea { resize: vertical; min-height: 4rem; }
|
|||||||
::-webkit-scrollbar-thumb:hover { background: var(--primary-hover); }
|
::-webkit-scrollbar-thumb:hover { background: var(--primary-hover); }
|
||||||
|
|
||||||
/* === Responsive === */
|
/* === Responsive === */
|
||||||
|
@media (max-width: 540px) {
|
||||||
|
header.site { flex-wrap: wrap; }
|
||||||
|
.tagline { flex-basis: 100%; }
|
||||||
|
}
|
||||||
|
|
||||||
@media (max-width: 480px) {
|
@media (max-width: 480px) {
|
||||||
.result-row { flex-wrap: wrap; }
|
.result-row { flex-wrap: wrap; }
|
||||||
.result-row .when { width: auto; }
|
.result-row .when { width: auto; }
|
||||||
|
|||||||
Reference in New Issue
Block a user