mirror of
https://origin.cursor.com/mrdevmx/panels.git
synced 2026-10-09 10:43:18 +00:00
829 lines
18 KiB
Vue
829 lines
18 KiB
Vue
<template>
|
|
<div class="panel-login" :class="{ 'is-dark': isDark, 'is-theme-switching': isThemeSwitching }">
|
|
<section class="mosaic">
|
|
<div class="mosaic-grid" aria-hidden="true">
|
|
<svg
|
|
v-for="(tone, i) in mosaicCells"
|
|
:key="i"
|
|
class="mosaic-motif"
|
|
:class="tone ? `is-${tone}` : undefined"
|
|
viewBox="0 0 164 164"
|
|
:style="{ transitionDelay: `${(i % 5) * 35}ms` }"
|
|
>
|
|
<!-- Mismas proporciones que el logo: celda 76, hueco 12, rx 18 -->
|
|
<rect class="petal" x="0" y="0" width="76" height="76" rx="18" />
|
|
<rect class="petal" x="88" y="0" width="76" height="76" rx="18" />
|
|
<rect class="petal" x="0" y="88" width="76" height="76" rx="18" />
|
|
<rect class="petal" x="88" y="88" width="76" height="76" rx="18" />
|
|
</svg>
|
|
</div>
|
|
<div class="mosaic-brand">
|
|
<div class="mosaic-brand-mark">
|
|
<img src="/brand/panel-icon-symmetric.svg" alt="" width="40" height="40" draggable="false">
|
|
<span>PANELS</span>
|
|
</div>
|
|
<p class="mosaic-brand-expand">
|
|
<span>P</span>lataforma de
|
|
<span>A</span>dministración de
|
|
<span>N</span>egocios,
|
|
<span>E</span>jecución,
|
|
<span>L</span>ogística y
|
|
<span>S</span>eguimiento
|
|
</p>
|
|
</div>
|
|
<div class="mosaic-copy">
|
|
<h2>Control total de<br>tus proyectos.</h2>
|
|
<p>Obra, personal, nómina y presupuestos<br>en un solo lugar.</p>
|
|
</div>
|
|
</section>
|
|
|
|
<section class="panel">
|
|
<div class="theme-toggle" role="group" aria-label="Tema">
|
|
<button type="button" :class="{ active: !isDark }" aria-label="Tema claro" @click="setDark(false)">
|
|
<i class="pi pi-sun" />
|
|
</button>
|
|
<button type="button" :class="{ active: isDark }" aria-label="Tema oscuro" @click="setDark(true)">
|
|
<i class="pi pi-moon" />
|
|
</button>
|
|
</div>
|
|
|
|
<form class="form" @submit.prevent="onSubmit">
|
|
<header class="form-head">
|
|
<h1>Bienvenido de nuevo</h1>
|
|
<p>Ingresa tus datos para continuar</p>
|
|
</header>
|
|
|
|
<label class="field">
|
|
<span>Usuario</span>
|
|
<span class="control">
|
|
<i class="pi pi-user" />
|
|
<input
|
|
v-model="username"
|
|
type="text"
|
|
name="username"
|
|
autocomplete="username"
|
|
spellcheck="false"
|
|
placeholder="Ingresa tu usuario"
|
|
>
|
|
</span>
|
|
</label>
|
|
|
|
<label class="field">
|
|
<span>Contraseña</span>
|
|
<span class="control">
|
|
<i class="pi pi-lock" />
|
|
<input
|
|
v-model="password"
|
|
:type="showPassword ? 'text' : 'password'"
|
|
name="password"
|
|
autocomplete="off"
|
|
placeholder="Ingresa tu contraseña"
|
|
>
|
|
<button
|
|
type="button"
|
|
class="toggle-pass"
|
|
:aria-label="showPassword ? 'Ocultar contraseña' : 'Mostrar contraseña'"
|
|
@click="showPassword = !showPassword"
|
|
>
|
|
<i :class="showPassword ? 'pi pi-eye-slash' : 'pi pi-eye'" />
|
|
</button>
|
|
</span>
|
|
</label>
|
|
|
|
<div class="form-row">
|
|
<label class="remember">
|
|
<input v-model="remember" type="checkbox">
|
|
<span class="check" />
|
|
Recordarme
|
|
</label>
|
|
<button type="button" class="forgot" @click="onForgot">¿Olvidaste tu contraseña?</button>
|
|
</div>
|
|
|
|
<p v-if="forgotHint" class="hint">{{ forgotHint }}</p>
|
|
<p v-if="error" class="error">{{ error }}</p>
|
|
|
|
<button type="submit" class="submit" :disabled="loading">
|
|
<span v-if="loading" class="spinner" />
|
|
Iniciar sesión
|
|
</button>
|
|
|
|
<p class="secure">
|
|
<i class="pi pi-lock" />
|
|
Acceso seguro
|
|
</p>
|
|
</form>
|
|
|
|
<p class="copyright">© {{ year }} PANELS</p>
|
|
</section>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
definePageMeta({ layout: "auth" });
|
|
|
|
useHead({
|
|
title: "Iniciar sesión · PANELS",
|
|
link: [
|
|
{
|
|
rel: "stylesheet",
|
|
href: "https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap",
|
|
},
|
|
],
|
|
});
|
|
|
|
type MosaicTone = "" | "pale" | "mid" | "strong";
|
|
|
|
const COLS = 7;
|
|
const ROWS = 12;
|
|
const CELL_COUNT = COLS * ROWS;
|
|
const TONE_STEPS: MosaicTone[] = ["", "pale", "mid", "strong"];
|
|
const ACCENT_COUNT = 10;
|
|
|
|
const INITIAL_ACCENTS: Record<string, MosaicTone> = {
|
|
"0-5": "pale",
|
|
"1-2": "mid",
|
|
"1-6": "strong",
|
|
"2-4": "pale",
|
|
"3-1": "strong",
|
|
"4-5": "pale",
|
|
"5-2": "mid",
|
|
"5-6": "strong",
|
|
"7-3": "pale",
|
|
"8-0": "mid",
|
|
"8-5": "strong",
|
|
"10-2": "pale",
|
|
"10-6": "mid",
|
|
};
|
|
|
|
function buildInitialMosaic(): MosaicTone[] {
|
|
return Array.from({ length: CELL_COUNT }, (_, i) => {
|
|
const row = Math.floor(i / COLS);
|
|
const col = i % COLS;
|
|
return INITIAL_ACCENTS[`${row}-${col}`] ?? "";
|
|
});
|
|
}
|
|
|
|
function toneIndex(tone: MosaicTone): number {
|
|
return TONE_STEPS.indexOf(tone);
|
|
}
|
|
|
|
function stepTone(tone: MosaicTone, delta: -1 | 1): MosaicTone {
|
|
const next = Math.min(TONE_STEPS.length - 1, Math.max(0, toneIndex(tone) + delta));
|
|
return TONE_STEPS[next]!;
|
|
}
|
|
|
|
function shuffleIndices(indices: number[]): number[] {
|
|
const next = [...indices];
|
|
for (let i = next.length - 1; i > 0; i--) {
|
|
const j = Math.floor(Math.random() * (i + 1));
|
|
[next[i], next[j]] = [next[j]!, next[i]!];
|
|
}
|
|
return next;
|
|
}
|
|
|
|
const mosaicCells = ref<MosaicTone[]>(buildInitialMosaic());
|
|
let mosaicTimer: ReturnType<typeof setInterval> | null = null;
|
|
|
|
/** Solo un escalón de tono por celda, pocos cambios por tick → drift suave. */
|
|
function driftMosaic() {
|
|
const next = [...mosaicCells.value];
|
|
const litCount = next.filter(Boolean).length;
|
|
const candidates = shuffleIndices(Array.from({ length: CELL_COUNT }, (_, i) => i));
|
|
let changed = 0;
|
|
const maxChanges = 3;
|
|
|
|
for (const i of candidates) {
|
|
if (changed >= maxChanges) break;
|
|
const tone = next[i]!;
|
|
const idx = toneIndex(tone);
|
|
let delta: -1 | 1 | 0 = 0;
|
|
|
|
if (litCount > ACCENT_COUNT + 1 && idx > 0) {
|
|
delta = -1;
|
|
} else if (litCount < ACCENT_COUNT - 1 && idx === 0) {
|
|
delta = 1;
|
|
} else if (idx === 0) {
|
|
if (Math.random() < 0.35) delta = 1;
|
|
} else if (idx === TONE_STEPS.length - 1) {
|
|
if (Math.random() < 0.55) delta = -1;
|
|
} else {
|
|
delta = Math.random() < 0.5 ? 1 : -1;
|
|
}
|
|
|
|
if (!delta) continue;
|
|
// Evita encender de más si ya hay bastantes acentos.
|
|
if (delta === 1 && idx === 0 && litCount >= ACCENT_COUNT + 1) continue;
|
|
next[i] = stepTone(tone, delta);
|
|
changed++;
|
|
}
|
|
|
|
mosaicCells.value = next;
|
|
}
|
|
|
|
const { login } = useAuth();
|
|
const { isDark, setDark: applyTheme } = useTheme();
|
|
const username = ref("");
|
|
const password = ref("");
|
|
const remember = ref(false);
|
|
const showPassword = ref(false);
|
|
const isThemeSwitching = ref(false);
|
|
const error = ref("");
|
|
const forgotHint = ref("");
|
|
const loading = ref(false);
|
|
const year = new Date().getFullYear();
|
|
let themeSwitchTimer: ReturnType<typeof setTimeout> | null = null;
|
|
|
|
onMounted(() => {
|
|
const savedUser = localStorage.getItem("panel-login-remember");
|
|
if (savedUser) {
|
|
username.value = savedUser;
|
|
remember.value = true;
|
|
}
|
|
|
|
const reduceMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
|
|
if (!reduceMotion) {
|
|
mosaicTimer = setInterval(driftMosaic, 1400);
|
|
}
|
|
});
|
|
|
|
onBeforeUnmount(() => {
|
|
if (mosaicTimer) clearInterval(mosaicTimer);
|
|
if (themeSwitchTimer) clearTimeout(themeSwitchTimer);
|
|
});
|
|
|
|
function setDark(value: boolean) {
|
|
if (value === isDark.value) return;
|
|
// Sin transición al cambiar tema: evita el flash de cuadros claros sobre fondo oscuro.
|
|
isThemeSwitching.value = true;
|
|
applyTheme(value);
|
|
if (themeSwitchTimer) clearTimeout(themeSwitchTimer);
|
|
themeSwitchTimer = setTimeout(() => {
|
|
isThemeSwitching.value = false;
|
|
themeSwitchTimer = null;
|
|
}, 50);
|
|
}
|
|
|
|
function onForgot() {
|
|
forgotHint.value = "Pide al administrador que restablezca tu acceso.";
|
|
}
|
|
|
|
async function onSubmit() {
|
|
error.value = "";
|
|
forgotHint.value = "";
|
|
if (!username.value.trim() || !password.value) {
|
|
error.value = "Ingresa tu usuario y contraseña";
|
|
return;
|
|
}
|
|
loading.value = true;
|
|
try {
|
|
if (remember.value) localStorage.setItem("panel-login-remember", username.value.trim());
|
|
else localStorage.removeItem("panel-login-remember");
|
|
await login(username.value.trim(), password.value.trim());
|
|
const { user } = useAuth();
|
|
const { api } = useApi();
|
|
if (user.value?.role === "platform_admin" && user.value?.realm === "platform") {
|
|
user.value = null;
|
|
try {
|
|
await api("/v1/auth/logout", { method: "POST" });
|
|
} catch {
|
|
/* ignore */
|
|
}
|
|
window.location.href = "http://localhost:3001/login";
|
|
return;
|
|
}
|
|
if (user.value?.must_change_password) {
|
|
await navigateTo("/cambiar-password");
|
|
return;
|
|
}
|
|
await navigateTo("/");
|
|
} catch (e: unknown) {
|
|
error.value = e instanceof Error ? e.message : "No se pudo entrar";
|
|
} finally {
|
|
loading.value = false;
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<style scoped>
|
|
.panel-login {
|
|
--blue: #185ee8;
|
|
--text: #0b1127;
|
|
--muted: #64748b;
|
|
--soft: #94a3b8;
|
|
--line: #e1e2e6;
|
|
--right-bg: #ffffff;
|
|
--left-bg: #ffffff;
|
|
--tile: #e8eefc;
|
|
--tile-pale: #c8d8fb;
|
|
--tile-mid: #8eb4ff;
|
|
--tile-strong: #4f86f7;
|
|
--input-bg: #ffffff;
|
|
--check-bg: #ffffff;
|
|
--toggle-bg: #ffffff;
|
|
--toggle-line: #e5e7eb;
|
|
--toggle-active: #eaf0fe;
|
|
--toggle-icon: #94a3b8;
|
|
--toggle-icon-active: #334155;
|
|
min-height: 100vh;
|
|
min-height: 100dvh;
|
|
display: grid;
|
|
grid-template-columns: minmax(380px, 46fr) minmax(420px, 54fr);
|
|
font-family: Inter, ui-sans-serif, system-ui, "Segoe UI", sans-serif;
|
|
font-size: 16px;
|
|
color: var(--text);
|
|
background: var(--right-bg);
|
|
overflow: hidden;
|
|
box-sizing: border-box;
|
|
}
|
|
|
|
.panel-login *,
|
|
.panel-login *::before,
|
|
.panel-login *::after {
|
|
box-sizing: border-box;
|
|
}
|
|
|
|
.panel-login.is-dark {
|
|
--text: #f8fafc;
|
|
--muted: #94a3b8;
|
|
--soft: #64748b;
|
|
--line: #243044;
|
|
--right-bg: #0f172a;
|
|
--left-bg: #0b1220;
|
|
--tile: #1a2744;
|
|
--tile-pale: #243b66;
|
|
--tile-mid: #2563eb;
|
|
--tile-strong: #60a5fa;
|
|
--input-bg: #111b2e;
|
|
--check-bg: #111b2e;
|
|
--toggle-bg: #111b2e;
|
|
--toggle-line: #243044;
|
|
--toggle-active: #1e3a5f;
|
|
--toggle-icon: #64748b;
|
|
--toggle-icon-active: #e2e8f0;
|
|
}
|
|
|
|
.mosaic {
|
|
position: relative;
|
|
background: var(--left-bg);
|
|
overflow: hidden;
|
|
display: flex;
|
|
flex-direction: column;
|
|
justify-content: space-between;
|
|
padding: 28px 32px 44px;
|
|
}
|
|
|
|
.mosaic-grid {
|
|
position: absolute;
|
|
inset: -12px;
|
|
display: grid;
|
|
grid-template-columns: repeat(7, minmax(0, 1fr));
|
|
grid-auto-rows: min-content;
|
|
align-content: start;
|
|
gap: 10px;
|
|
padding: 10px;
|
|
pointer-events: none;
|
|
}
|
|
|
|
.mosaic-motif {
|
|
width: 100%;
|
|
aspect-ratio: 1;
|
|
display: block;
|
|
overflow: visible;
|
|
filter: drop-shadow(0 0 0 transparent);
|
|
transition:
|
|
filter 1.35s cubic-bezier(0.4, 0, 0.2, 1);
|
|
will-change: filter;
|
|
}
|
|
|
|
.mosaic-motif .petal {
|
|
fill: var(--tile);
|
|
transition: fill 1.35s cubic-bezier(0.4, 0, 0.2, 1);
|
|
}
|
|
|
|
.mosaic-motif.is-pale .petal {
|
|
fill: var(--tile-pale);
|
|
}
|
|
|
|
.mosaic-motif.is-mid .petal {
|
|
fill: var(--tile-mid);
|
|
}
|
|
|
|
.mosaic-motif.is-strong .petal {
|
|
fill: var(--tile-strong);
|
|
}
|
|
|
|
.mosaic-motif.is-pale {
|
|
filter: drop-shadow(0 0 10px rgba(138, 180, 254, 0.28));
|
|
}
|
|
|
|
.mosaic-motif.is-mid {
|
|
filter: drop-shadow(0 0 14px rgba(79, 134, 247, 0.4));
|
|
}
|
|
|
|
.mosaic-motif.is-strong {
|
|
filter: drop-shadow(0 0 18px rgba(47, 111, 237, 0.55));
|
|
}
|
|
|
|
.panel-login.is-dark .mosaic-motif.is-pale {
|
|
filter: drop-shadow(0 0 12px rgba(59, 130, 246, 0.35));
|
|
}
|
|
|
|
.panel-login.is-dark .mosaic-motif.is-mid {
|
|
filter: drop-shadow(0 0 16px rgba(59, 130, 246, 0.5));
|
|
}
|
|
|
|
.panel-login.is-dark .mosaic-motif.is-strong {
|
|
filter: drop-shadow(0 0 22px rgba(96, 165, 250, 0.65));
|
|
}
|
|
|
|
@media (prefers-reduced-motion: reduce) {
|
|
.mosaic-motif,
|
|
.mosaic-motif .petal {
|
|
transition: none;
|
|
}
|
|
}
|
|
|
|
.panel-login.is-theme-switching .mosaic-motif,
|
|
.panel-login.is-theme-switching .mosaic-motif .petal {
|
|
transition: none !important;
|
|
transition-delay: 0s !important;
|
|
}
|
|
|
|
.mosaic-brand,
|
|
.mosaic-copy {
|
|
position: relative;
|
|
z-index: 1;
|
|
}
|
|
|
|
.mosaic-brand {
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: flex-start;
|
|
gap: 8px;
|
|
max-width: 300px;
|
|
}
|
|
|
|
.mosaic-brand-mark {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 12px;
|
|
}
|
|
|
|
.mosaic-brand-mark img {
|
|
width: 40px;
|
|
height: 40px;
|
|
display: block;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.mosaic-brand-mark span {
|
|
font-size: 20px;
|
|
font-weight: 800;
|
|
letter-spacing: 0.06em;
|
|
color: var(--text);
|
|
}
|
|
|
|
.mosaic-brand-expand {
|
|
margin: 0;
|
|
padding-left: 52px;
|
|
font-size: 11px;
|
|
line-height: 1.45;
|
|
font-weight: 500;
|
|
color: var(--soft);
|
|
}
|
|
|
|
.mosaic-brand-expand span {
|
|
color: var(--muted);
|
|
font-weight: 700;
|
|
}
|
|
|
|
.mosaic-copy h2 {
|
|
margin: 0 0 12px;
|
|
font-size: 36px;
|
|
font-weight: 800;
|
|
line-height: 1.15;
|
|
letter-spacing: -0.03em;
|
|
color: var(--text);
|
|
}
|
|
|
|
.mosaic-copy p {
|
|
margin: 0;
|
|
font-size: 15px;
|
|
line-height: 1.5;
|
|
color: #475569;
|
|
}
|
|
|
|
.panel-login.is-dark .mosaic-copy p {
|
|
color: #94a3b8;
|
|
}
|
|
|
|
.panel {
|
|
position: relative;
|
|
background: var(--right-bg);
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
}
|
|
|
|
.theme-toggle {
|
|
position: absolute;
|
|
top: 24px;
|
|
right: 24px;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 2px;
|
|
padding: 4px;
|
|
border: 1px solid var(--toggle-line);
|
|
border-radius: 999px;
|
|
background: var(--toggle-bg);
|
|
}
|
|
|
|
.theme-toggle button {
|
|
width: 34px;
|
|
height: 30px;
|
|
border: 0;
|
|
background: transparent;
|
|
border-radius: 9px;
|
|
color: var(--toggle-icon);
|
|
cursor: pointer;
|
|
display: grid;
|
|
place-items: center;
|
|
font-size: 13px;
|
|
}
|
|
|
|
.theme-toggle button.active {
|
|
background: var(--toggle-active);
|
|
color: var(--toggle-icon-active);
|
|
}
|
|
|
|
.form {
|
|
width: 100%;
|
|
max-width: 400px;
|
|
padding: 0 24px;
|
|
}
|
|
|
|
.form-head {
|
|
text-align: center;
|
|
margin-bottom: 40px;
|
|
}
|
|
|
|
.form-head h1 {
|
|
margin: 0 0 8px;
|
|
font-size: 32px;
|
|
font-weight: 800;
|
|
letter-spacing: -0.03em;
|
|
color: var(--text);
|
|
}
|
|
|
|
.form-head p {
|
|
margin: 0;
|
|
font-size: 15px;
|
|
color: var(--muted);
|
|
}
|
|
|
|
.field {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 8px;
|
|
margin-bottom: 20px;
|
|
}
|
|
|
|
.field > span:first-child {
|
|
font-size: 11px;
|
|
font-weight: 600;
|
|
letter-spacing: 0.08em;
|
|
text-transform: uppercase;
|
|
color: var(--muted);
|
|
}
|
|
|
|
.control {
|
|
position: relative;
|
|
display: block;
|
|
}
|
|
|
|
.control > i {
|
|
position: absolute;
|
|
left: 14px;
|
|
top: 50%;
|
|
transform: translateY(-50%);
|
|
color: var(--soft);
|
|
font-size: 14px;
|
|
pointer-events: none;
|
|
}
|
|
|
|
.control input {
|
|
width: 100%;
|
|
height: 48px;
|
|
box-sizing: border-box;
|
|
border: 1px solid var(--line);
|
|
border-radius: 12px;
|
|
background: var(--input-bg);
|
|
color: var(--text);
|
|
padding: 0 44px 0 42px;
|
|
font: inherit;
|
|
font-size: 14px;
|
|
outline: none;
|
|
}
|
|
|
|
.control input::placeholder {
|
|
color: var(--soft);
|
|
}
|
|
|
|
.control input:focus {
|
|
border-color: var(--blue);
|
|
box-shadow: 0 0 0 3px rgba(47, 111, 237, 0.12);
|
|
}
|
|
|
|
.toggle-pass {
|
|
position: absolute;
|
|
right: 8px;
|
|
top: 50%;
|
|
transform: translateY(-50%);
|
|
width: 32px;
|
|
height: 32px;
|
|
border: 0;
|
|
background: transparent;
|
|
color: var(--soft);
|
|
cursor: pointer;
|
|
display: grid;
|
|
place-items: center;
|
|
font-size: 14px;
|
|
}
|
|
|
|
.form-row {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
gap: 12px;
|
|
margin: 4px 0 24px;
|
|
}
|
|
|
|
.remember {
|
|
position: relative;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
font-size: 14px;
|
|
color: var(--text);
|
|
cursor: pointer;
|
|
user-select: none;
|
|
}
|
|
|
|
.remember input {
|
|
position: absolute;
|
|
opacity: 0;
|
|
pointer-events: none;
|
|
}
|
|
|
|
.check {
|
|
width: 16px;
|
|
height: 16px;
|
|
border: 1px solid var(--line);
|
|
border-radius: 4px;
|
|
background: var(--check-bg);
|
|
box-sizing: border-box;
|
|
display: grid;
|
|
place-items: center;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.remember input:checked + .check {
|
|
background: var(--blue);
|
|
border-color: var(--blue);
|
|
}
|
|
|
|
.remember input:checked + .check::after {
|
|
content: "";
|
|
width: 8px;
|
|
height: 5px;
|
|
border: 2px solid #fff;
|
|
border-top: 0;
|
|
border-right: 0;
|
|
transform: rotate(-45deg) translate(0, -1px);
|
|
}
|
|
|
|
.forgot {
|
|
border: 0;
|
|
background: transparent;
|
|
color: var(--blue);
|
|
font: inherit;
|
|
font-size: 14px;
|
|
font-weight: 500;
|
|
cursor: pointer;
|
|
padding: 0;
|
|
}
|
|
|
|
.hint,
|
|
.error {
|
|
margin: -8px 0 16px;
|
|
font-size: 13px;
|
|
line-height: 1.4;
|
|
}
|
|
|
|
.hint { color: var(--muted); }
|
|
.error { color: #dc2626; }
|
|
|
|
.submit {
|
|
width: 100%;
|
|
height: 48px;
|
|
border: 0;
|
|
border-radius: 12px;
|
|
background: var(--blue);
|
|
color: #fff;
|
|
font: inherit;
|
|
font-size: 15px;
|
|
font-weight: 600;
|
|
cursor: pointer;
|
|
display: inline-flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
gap: 8px;
|
|
}
|
|
|
|
.panel-login button,
|
|
.panel-login input {
|
|
appearance: none;
|
|
-webkit-appearance: none;
|
|
}
|
|
|
|
.submit:hover:not(:disabled) {
|
|
background: #1454d0;
|
|
}
|
|
|
|
.control input:-webkit-autofill,
|
|
.control input:-webkit-autofill:hover,
|
|
.control input:-webkit-autofill:focus {
|
|
-webkit-box-shadow: 0 0 0 30px var(--input-bg) inset;
|
|
-webkit-text-fill-color: var(--text);
|
|
caret-color: var(--text);
|
|
}
|
|
|
|
.submit:disabled {
|
|
opacity: 0.72;
|
|
cursor: default;
|
|
}
|
|
|
|
.spinner {
|
|
width: 14px;
|
|
height: 14px;
|
|
border: 2px solid rgba(255, 255, 255, 0.35);
|
|
border-top-color: #fff;
|
|
border-radius: 50%;
|
|
animation: spin 0.7s linear infinite;
|
|
}
|
|
|
|
.secure {
|
|
margin: 20px 0 0;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
gap: 8px;
|
|
font-size: 13px;
|
|
color: var(--soft);
|
|
}
|
|
|
|
.secure i {
|
|
font-size: 12px;
|
|
}
|
|
|
|
.copyright {
|
|
position: absolute;
|
|
left: 0;
|
|
right: 0;
|
|
bottom: 28px;
|
|
margin: 0;
|
|
text-align: center;
|
|
font-size: 13px;
|
|
color: var(--soft);
|
|
}
|
|
|
|
@keyframes spin {
|
|
to { transform: rotate(360deg); }
|
|
}
|
|
|
|
@media (max-width: 960px) {
|
|
.panel-login {
|
|
grid-template-columns: 1fr;
|
|
overflow: auto;
|
|
}
|
|
.mosaic {
|
|
min-height: 240px;
|
|
padding: 24px 24px 28px;
|
|
}
|
|
.mosaic-copy h2 {
|
|
font-size: 28px;
|
|
}
|
|
.theme-toggle {
|
|
top: 16px;
|
|
right: 16px;
|
|
}
|
|
.form {
|
|
padding: 72px 24px 80px;
|
|
}
|
|
.copyright {
|
|
position: relative;
|
|
bottom: auto;
|
|
padding-bottom: 24px;
|
|
}
|
|
}
|
|
</style>
|