Files
MyIP/frontend/style/style.css
T
Claude 435d9f6413 Chore: drop dead nav locale keys, one isFireBaseSet, page-bg comment
nav.Title / PingTest / MTRTest / RuleTest had no reader in any locale.
The store takes isFireBaseSet from firebase-init.js instead of
recomputing it in checkFirebaseEnv(). The --page-bg comment no longer
claims index.html's colours must match it; they are tuned separately.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013GbLTGMZ45MV78qd2tHoco
2026-10-05 17:02:50 +00:00

339 lines
12 KiB
CSS

/* === Tailwind v4 + shadcn-vue === */
@import 'tailwindcss';
@import 'tw-animate-css';
/* Base styles */
@layer base {
h1 { font-size: 2.5rem; font-weight: 500; line-height: 1.2; margin-bottom: 0.5rem; }
h2 { font-size: 2rem; font-weight: 500; line-height: 1.2; margin-bottom: 0.5rem; }
h3 { font-size: 1.75rem; font-weight: 500; line-height: 1.2; margin-bottom: 0.5rem; }
h4 { font-size: 1.5rem; font-weight: 500; line-height: 1.2; margin-bottom: 0.5rem; }
h5 { font-size: 1.25rem; font-weight: 500; line-height: 1.2; margin-bottom: 0.5rem; }
h6 { font-size: 1rem; font-weight: 500; line-height: 1.2; margin-bottom: 0.5rem; }
p { margin-bottom: 1rem; }
/* Required patch for shadcn-vue + Tailwind v4:
In Tailwind v4, the `border` utility color falls back to currentColor (black/white),
unlike v3 which defaults to gray-200. shadcn-vue components (Card / DropdownMenuContent /
Dialog, etc.) have bare `border` classes without color, which will inherit text color and
become "black borders". Bind the default border color back to design token --border to
restore expected behavior.
Reference: https://ui.shadcn.com/docs/tailwind-v4 */
*, ::before, ::after {
border-color: var(--border);
}
}
/* Dark mode: triggered by .dark class (synchronized from store.isDarkMode to <html>) */
@custom-variant dark (&:where(.dark, .dark *));
/* Design tokens — shadcn-vue standard CSS variables + project custom semantic colors
---------------------------------------------------------
Custom token rules:
- info = information/in-progress/waiting (paired with animate-ping status light) ≈ sky-500
- success = success/OK-fast ≈ green-500
- warning = warning/OK-slow ≈ amber-500
- destructive continue using shadcn primitives, corresponding to fail / error ≈ red-500
Each semantic color has a foreground pair, which automatically corrects the text color when used in bg-* ≈ amber-500 (foreground is dark because amber is light; sky / green / red are dark, so foreground is white). The light/dark mode is defined independently by :root / .dark, and tokens do not need to write dark: variants.
*/
:root {
color-scheme: light;
--background: oklch(1 0 0);
--foreground: oklch(0.145 0 0);
--card: oklch(1 0 0);
--card-foreground: oklch(0.145 0 0);
--popover: oklch(1 0 0);
--popover-foreground: oklch(0.145 0 0);
--primary: oklch(0.205 0 0);
--primary-foreground: oklch(0.985 0 0);
--secondary: oklch(0.97 0 0);
--secondary-foreground: oklch(0.205 0 0);
--muted: oklch(0.97 0 0);
--muted-foreground: oklch(0.556 0 0);
--accent: oklch(0.97 0 0);
--accent-foreground: oklch(0.205 0 0);
--destructive: oklch(0.577 0.245 27.325);
--destructive-foreground: oklch(0.985 0 0);
/* Project custom semantic colors (light mode, corresponding to Tailwind *-500 oklch values) */
--info: oklch(0.685 0.169 237.323); /* sky-500 */
--info-foreground: oklch(0.985 0 0);
--success: oklch(0.63 0.17 149); /* green-500 */
--success-foreground: oklch(0.985 0 0);
--warning: oklch(0.769 0.188 70.08); /* amber-500 */
--warning-foreground: oklch(0.145 0 0);
--action: oklch(0.623 0.214 259.815); /* blue-500 —— for "run action" */
--action-foreground: oklch(0.985 0 0);
--border: oklch(0.922 0 0);
--input: oklch(0.922 0 0);
--ring: oklch(0.708 0 0);
--radius: 0.5rem;
/* Canonical page background: the installed app's Nav (`bg-page-bg`) and
the manifest's theme_color use it. index.html's html background and
theme-color are tuned separately on purpose. */
--page-bg: #f4f4f4;
}
.dark {
color-scheme: dark;
--background: oklch(0.145 0 0);
--foreground: oklch(0.985 0 0);
--card: oklch(0.205 0 0);
--card-foreground: oklch(0.985 0 0);
--popover: oklch(0.205 0 0);
--popover-foreground: oklch(0.985 0 0);
--primary: oklch(0.985 0 0);
--primary-foreground: oklch(0.205 0 0);
--secondary: oklch(0.269 0 0);
--secondary-foreground: oklch(0.985 0 0);
--muted: oklch(0.269 0 0);
--muted-foreground: oklch(0.708 0 0);
--accent: oklch(0.269 0 0);
--accent-foreground: oklch(0.985 0 0);
--destructive: oklch(0.704 0.191 22.216);
--destructive-foreground: oklch(0.985 0 0);
/* dark mode slightly brightened (corresponding to Tailwind *-400, better readability in dark background) */
--info: oklch(0.746 0.16 232.661); /* sky-400 */
--info-foreground: oklch(0.145 0 0);
--success: oklch(0.792 0.209 151.711); /* green-400 */
--success-foreground: oklch(0.145 0 0);
--warning: oklch(0.828 0.189 84.429); /* amber-400 */
--warning-foreground: oklch(0.145 0 0);
--action: oklch(0.623 0.214 259.815); /* blue-500 (same as light mode) */
--action-foreground: oklch(0.985 0 0); /* white text */
--border: oklch(1 0 0 / 0.1);
--input: oklch(1 0 0 / 0.15);
--ring: oklch(0.556 0 0);
--page-bg: #0e0e0e;
}
@theme inline {
--color-background: var(--background);
--color-foreground: var(--foreground);
--color-card: var(--card);
--color-card-foreground: var(--card-foreground);
--color-popover: var(--popover);
--color-popover-foreground: var(--popover-foreground);
--color-primary: var(--primary);
--color-primary-foreground: var(--primary-foreground);
--color-secondary: var(--secondary);
--color-secondary-foreground: var(--secondary-foreground);
--color-muted: var(--muted);
--color-muted-foreground: var(--muted-foreground);
--color-accent: var(--accent);
--color-accent-foreground: var(--accent-foreground);
--color-destructive: var(--destructive);
--color-destructive-foreground: var(--destructive-foreground);
--color-info: var(--info);
--color-info-foreground: var(--info-foreground);
--color-success: var(--success);
--color-success-foreground: var(--success-foreground);
--color-warning: var(--warning);
--color-warning-foreground: var(--warning-foreground);
--color-action: var(--action);
--color-action-foreground: var(--action-foreground);
--color-border: var(--border);
--color-input: var(--input);
--color-ring: var(--ring);
--color-page-bg: var(--page-bg);
--radius-lg: var(--radius);
--radius-md: calc(var(--radius) - 2px);
--radius-sm: calc(var(--radius) - 4px);
}
/* vaul default injects @media (hover:hover) and (pointer:fine) { user-select: none }
into [data-vaul-drawer] to avoid conflict between drag gestures and text selection.
We set handleOnly to true by default in the Drawer component (only the handle can be dragged, and the main content does not respond to dragging), so this restriction is a bug → users want to select text in the drawer on desktop but cannot.
Two pitfalls:
1. vaul's <style> is appended to document.head in JS, so its source order is later than style.css; when specificity is the same, the latter wins → it must be forced with !important
2. user-select: auto is parsed as none when the parent is none (MDN rule) → it must be explicitly set to text; and it must be explicitly overridden on descendants, not relying on inheritance */
@media (hover: hover) and (pointer: fine) {
[data-vaul-drawer],
[data-vaul-drawer] * {
-webkit-user-select: text !important;
user-select: text !important;
}
}
/* === Base end === */
/* === Custom CSS === */
/* InfoMask — CSS-only privacy blur.
The toggle widget mirrors its on/off state to <html data-mask-level="1">; IP
cells throughout the app mark themselves with data-mask="ip". A blur filter
then renders those values unreadable in screenshots without mutating any data,
so toggling on/off is instantaneous and screenshot-safe. */
[data-mask-level="1"] [data-mask="ip"] {
filter: blur(7px) saturate(0.6);
-webkit-user-select: none;
user-select: none;
pointer-events: none;
transition: filter 0.25s ease;
}
@keyframes jn-collapsible-down {
from { height: 0; }
to { height: var(--reka-collapsible-content-height); }
}
@keyframes jn-collapsible-up {
from { height: var(--reka-collapsible-content-height); }
to { height: 0; }
}
@keyframes jn-accordion-down {
from { height: 0; }
to { height: var(--reka-accordion-content-height); }
}
@keyframes jn-accordion-up {
from { height: var(--reka-accordion-content-height); }
to { height: 0; }
}
.jn-collapsible-content[data-state='open'] {
animation: jn-collapsible-down 200ms ease-out;
}
.jn-collapsible-content[data-state='closed'] {
animation: jn-collapsible-up 200ms ease-out;
}
.jn-accordion-content[data-state='open'] {
animation: jn-accordion-down 200ms ease-out;
}
.jn-accordion-content[data-state='closed'] {
animation: jn-accordion-up 200ms ease-out;
}
.jn-card {
box-shadow: 0 0 10pt #0000001c;
height: 100%;
border: 1px solid #9c9c9c2e;
}
.jn-card.keyboard-hover {
border-color: rgba(20, 184, 108, 0.5);
outline: 2px solid rgba(20, 184, 108, 0.5);
outline-offset: -1px;
border-radius: var(--radius);
}
.jn-container {
padding-top: 2pt;
max-width: 1400px;
}
footer {
height: 100pt;
padding-top: 20pt;
}
@media only screen and (max-width: 768px) {
footer {
height: 140pt;
}
}
.body-dark-mode {
background-color: rgb(19, 21, 21) !important;
color: #e3e3e3 !important;
border-color: #757575;
}
.hidden {
display: none !important;
}
*:focus {
outline: none;
}
.speedtest-p {
margin-top: 10pt;
}
/* GitBook Docs Assistant embed (loaded on demand by use-docs-assistant.js).
The stock launcher is hidden — widgets/DocsAssistant.vue renders our own
toggle in its place, styled like the FAB dock's buttons. The panel is
pinned bottom-LEFT (the FAB stack owns the bottom-right), mirroring the
dock's inset off the 1600px content area and clearing the toggle by its
height plus a gap, with a height cap so it never runs off the top on
phones. dvh tracks mobile browser chrome as it collapses; the vh line is
the fallback for browsers without it. */
#gitbook-widget-button {
display: none !important;
}
#gitbook-widget-window {
--jn-gb-anchor: 2.25rem;
--jn-gb-inset: max(18px, (100vw - 1600px) / 2 + 18px);
right: auto !important;
left: var(--jn-gb-inset) !important;
bottom: calc(var(--jn-gb-anchor) + 3.25rem) !important;
max-height: calc(100vh - var(--jn-gb-anchor) - 3.25rem - env(safe-area-inset-top) - 1rem) !important;
max-height: calc(100dvh - var(--jn-gb-anchor) - 3.25rem - env(safe-area-inset-top) - 1rem) !important;
}
/* Page transitions (utils/page-transition.js), keyed by <html
data-page-transition>: "forward" slides the new page in from the right,
"back" mirrors it, "fade" cross-fades. The Nav is captured on its own and
shown in its new state at once, so only the page moves. */
.jn-site-nav {
view-transition-name: site-nav;
}
::view-transition-old(site-nav) {
display: none;
}
::view-transition-new(site-nav) {
animation: none;
}
@media (prefers-reduced-motion: no-preference) {
html[data-page-transition="forward"]::view-transition-old(root),
html[data-page-transition="forward"]::view-transition-new(root),
html[data-page-transition="back"]::view-transition-old(root),
html[data-page-transition="back"]::view-transition-new(root) {
mix-blend-mode: normal; /* no plus-lighter brightening where the pages overlap */
animation-duration: 260ms;
animation-timing-function: cubic-bezier(0.32, 0.72, 0, 1);
animation-fill-mode: both;
}
html[data-page-transition="forward"]::view-transition-old(root) {
animation-name: jn-page-drift-left;
}
html[data-page-transition="forward"]::view-transition-new(root) {
animation-name: jn-page-enter-right;
}
html[data-page-transition="back"]::view-transition-old(root) {
z-index: 1;
animation-name: jn-page-exit-right;
}
html[data-page-transition="back"]::view-transition-new(root) {
animation-name: jn-page-return-left;
}
html[data-page-transition="fade"]::view-transition-old(root) {
animation: jn-page-fade-out 180ms ease both;
}
html[data-page-transition="fade"]::view-transition-new(root) {
animation: jn-page-fade-in 180ms ease both;
}
}
@keyframes jn-page-enter-right {
from { transform: translateX(100%); }
to { transform: none; }
}
@keyframes jn-page-drift-left {
from { transform: none; opacity: 1; }
to { transform: translateX(-25%); opacity: 0.4; }
}
@keyframes jn-page-exit-right {
from { transform: none; }
to { transform: translateX(100%); }
}
@keyframes jn-page-return-left {
from { transform: translateX(-25%); opacity: 0.4; }
to { transform: none; opacity: 1; }
}
@keyframes jn-page-fade-out {
to { opacity: 0; }
}
@keyframes jn-page-fade-in {
from { opacity: 0; }
}