diff --git a/fusion_clock/controllers/clock_nfc_kiosk.py b/fusion_clock/controllers/clock_nfc_kiosk.py
index 32ab5e70..d04c6d99 100644
--- a/fusion_clock/controllers/clock_nfc_kiosk.py
+++ b/fusion_clock/controllers/clock_nfc_kiosk.py
@@ -74,9 +74,13 @@ class FusionClockNfcKiosk(http.Controller):
company = request.env.company
location = company.x_fclk_nfc_kiosk_location_id
+ company_logo_url = (
+ '/web/image/res.company/%s/logo' % company.id if company.logo else ''
+ )
values = {
'page_name': 'nfc_kiosk',
'company_name': company.name,
+ 'company_logo_url': company_logo_url,
'location_name': location.name if location else 'No location configured',
'location_configured': bool(location),
'photo_required': ICP.get_param('fusion_clock.nfc_photo_required', 'True') == 'True',
diff --git a/fusion_clock/static/src/js/fusion_clock_nfc_kiosk.js b/fusion_clock/static/src/js/fusion_clock_nfc_kiosk.js
index 47794566..46ce7965 100644
--- a/fusion_clock/static/src/js/fusion_clock_nfc_kiosk.js
+++ b/fusion_clock/static/src/js/fusion_clock_nfc_kiosk.js
@@ -39,6 +39,80 @@
}
debugLog("page loaded; debugEnabled=" + debugEnabled + " photoRequired=" + photoRequired + " NDEFReader=" + ("NDEFReader" in window));
+ // ──────────────────────────────────────────────────────────────
+ // Dominant-hue extraction from company logo
+ // Sets the CSS variable --nfc-h on so SCSS can interpolate
+ // the entire palette from the brand color. Falls back to default
+ // (220 = aurora-blue) if no logo or extraction fails.
+ // ──────────────────────────────────────────────────────────────
+ function rgbToHue(r, g, b) {
+ const rN = r / 255, gN = g / 255, bN = b / 255;
+ const max = Math.max(rN, gN, bN), min = Math.min(rN, gN, bN);
+ const d = max - min;
+ if (d === 0) return null; // grayscale, no hue info
+ let h;
+ if (max === rN) h = ((gN - bN) / d) % 6;
+ else if (max === gN) h = (bN - rN) / d + 2;
+ else h = (rN - gN) / d + 4;
+ h = Math.round(h * 60);
+ if (h < 0) h += 360;
+ return h;
+ }
+
+ function extractDominantHue(img) {
+ try {
+ const c = document.createElement("canvas");
+ const w = c.width = Math.min(img.naturalWidth, 200);
+ const h = c.height = Math.min(img.naturalHeight, 200);
+ const ctx = c.getContext("2d", { willReadFrequently: true });
+ ctx.drawImage(img, 0, 0, w, h);
+ const data = ctx.getImageData(0, 0, w, h).data;
+ let r = 0, g = 0, b = 0, count = 0;
+ for (let i = 0; i < data.length; i += 4) {
+ const a = data[i + 3];
+ if (a < 128) continue; // skip transparent
+ const red = data[i], green = data[i + 1], blue = data[i + 2];
+ const lum = (red + green + blue) / 3;
+ if (lum > 235 || lum < 25) continue; // skip near-white/near-black
+ const range = Math.max(red, green, blue) - Math.min(red, green, blue);
+ if (range < 25) continue; // skip near-grays
+ r += red; g += green; b += blue; count++;
+ }
+ if (count < 50) {
+ debugLog("hue extraction: too few colored pixels (" + count + "), using default");
+ return null;
+ }
+ const avgR = Math.round(r / count), avgG = Math.round(g / count), avgB = Math.round(b / count);
+ const hue = rgbToHue(avgR, avgG, avgB);
+ debugLog("hue extracted: rgb(" + avgR + "," + avgG + "," + avgB + ") → h=" + hue);
+ return hue;
+ } catch (e) {
+ debugLog("hue extraction failed: " + e.message);
+ return null;
+ }
+ }
+
+ function applyBrandHue(hue) {
+ if (hue == null) return;
+ document.documentElement.style.setProperty("--nfc-h", String(hue));
+ }
+
+ const logoImg = document.getElementById("nfc_company_logo");
+ if (logoImg) {
+ const tryExtract = () => {
+ const hue = extractDominantHue(logoImg);
+ applyBrandHue(hue);
+ };
+ if (logoImg.complete && logoImg.naturalWidth) {
+ tryExtract();
+ } else {
+ logoImg.addEventListener("load", tryExtract);
+ logoImg.addEventListener("error", () => debugLog("logo failed to load"));
+ }
+ } else {
+ debugLog("no company logo on page; using default hue");
+ }
+
// ──────────────────────────────────────────────────────────────
// State machine
// ──────────────────────────────────────────────────────────────
@@ -59,7 +133,16 @@
function renderIdle() {
stateContainer.innerHTML = `
-
⌐■
+
Tap your card to clock in or out
`;
@@ -67,7 +150,10 @@
function renderProcessing() {
stateContainer.innerHTML = `
- Reading card…
+
+ Reading card
+
+
`;
}
diff --git a/fusion_clock/static/src/scss/nfc_kiosk.scss b/fusion_clock/static/src/scss/nfc_kiosk.scss
index 8da2b7df..183313b3 100644
--- a/fusion_clock/static/src/scss/nfc_kiosk.scss
+++ b/fusion_clock/static/src/scss/nfc_kiosk.scss
@@ -1,31 +1,53 @@
-// NFC Clock Kiosk — always-dark, high-contrast.
-// Per CLAUDE.md: shop-floor kiosks need explicit hex (no var(--bs-*) which drift)
-// and we deliberately do NOT branch on $o-webclient-color-scheme — this is a
-// frontend bundle (not backend), and the kiosk is intentionally always dark
-// regardless of the user's color scheme preference.
+// NFC Clock Kiosk — premium glass + animated mesh, always-dark.
+//
+// CRITICAL: All styles in this file are scoped under `:has(#nfc_kiosk_root)`
+// to prevent leaking into other frontend pages. The previous version applied
+// `html,body { overflow:hidden; height:100vh }` and `header,footer{display:none}`
+// globally, which broke website scrolling and chrome on every frontend page.
+//
+// The single CSS custom property `--nfc-h` (hue, 0–360) is set by JS after
+// extracting the dominant color from the company logo. All colors interpolate
+// from that hue via HSL, so the entire palette adapts to the customer brand.
-$nfc-bg: #0b0d10;
-$nfc-panel: #15191f;
-$nfc-text: #ffffff;
-$nfc-text-muted: #9ba3ad;
-$nfc-success: #18a957;
-$nfc-error: #d9374e;
-$nfc-accent: #3b82f6;
-$nfc-border: #2a3038;
-
-html, body {
- background: $nfc-bg !important;
- color: $nfc-text;
- margin: 0;
- padding: 0;
- overflow: hidden;
- height: 100vh;
- font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
+// ─────────────────────────────────────────────────────────────────────
+// Defaults (overridden by JS once logo dominant-hue is extracted)
+// ─────────────────────────────────────────────────────────────────────
+:root {
+ --nfc-h: 220; // fallback aurora-blue hue
+ --nfc-bg: #0b0d10;
+ --nfc-text: #ffffff;
+ --nfc-text-muted: #9ba3ad;
+ --nfc-success: #18a957;
+ --nfc-error: #d9374e;
}
-.o_main_navbar, header, footer { display: none !important; }
+// ─────────────────────────────────────────────────────────────────────
+// Page-level styling — ONLY when the kiosk is on the page
+// ─────────────────────────────────────────────────────────────────────
+html:has(#nfc_kiosk_root) {
+ overflow: hidden;
+ height: 100%;
+ body {
+ overflow: hidden;
+ height: 100%;
+ margin: 0;
+ padding: 0;
+ background: var(--nfc-bg) !important;
+ color: var(--nfc-text);
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
+ }
+
+ // Hide site chrome on the kiosk page only
+ .o_main_navbar, header, footer, .o_header_standard, .o_footer { display: none !important; }
+}
+
+// ─────────────────────────────────────────────────────────────────────
+// Kiosk root container with animated mesh gradient background
+// ─────────────────────────────────────────────────────────────────────
.nfc-kiosk {
+ position: fixed;
+ inset: 0;
width: 100vw;
height: 100vh;
display: flex;
@@ -36,100 +58,312 @@ html, body {
box-sizing: border-box;
user-select: none;
-webkit-tap-highlight-color: transparent;
+ overflow: hidden;
+ background: var(--nfc-bg);
+
+ // Animated mesh gradient (drifts behind everything)
+ &::before {
+ content: "";
+ position: absolute;
+ inset: -15%;
+ background:
+ radial-gradient(circle at 20% 30%, hsla(var(--nfc-h), 75%, 40%, 0.55) 0%, transparent 45%),
+ radial-gradient(circle at 80% 20%, hsla(calc(var(--nfc-h) + 40), 65%, 35%, 0.50) 0%, transparent 50%),
+ radial-gradient(circle at 70% 75%, hsla(calc(var(--nfc-h) - 25), 70%, 35%, 0.45) 0%, transparent 55%),
+ radial-gradient(circle at 15% 85%, hsla(calc(var(--nfc-h) + 80), 60%, 30%, 0.40) 0%, transparent 50%);
+ filter: blur(60px) saturate(140%);
+ animation: nfc-mesh-drift 28s ease-in-out infinite alternate;
+ z-index: 0;
+ will-change: transform;
+ }
+
+ // Subtle vignette on top so edges don't feel washed out
+ &::after {
+ content: "";
+ position: absolute;
+ inset: 0;
+ background: radial-gradient(ellipse at center, transparent 50%, rgba(0,0,0,0.45) 100%);
+ z-index: 1;
+ pointer-events: none;
+ }
+
+ > * { position: relative; z-index: 2; }
}
-.nfc-kiosk__company {
+@keyframes nfc-mesh-drift {
+ 0% { transform: translate(0%, 0%) rotate(0deg) scale(1); }
+ 50% { transform: translate(3%, -2%) rotate(2deg) scale(1.05); }
+ 100% { transform: translate(-3%, 3%) rotate(-1deg) scale(0.98); }
+}
+
+// ─────────────────────────────────────────────────────────────────────
+// Header chrome — logo, time, date, location, settings
+// ─────────────────────────────────────────────────────────────────────
+.nfc-kiosk__logo {
position: absolute;
top: 1.5rem;
left: 50%;
transform: translateX(-50%);
- font-size: 1.25rem;
- color: $nfc-text-muted;
+ max-height: 64px;
+ max-width: 240px;
+ object-fit: contain;
+ filter: drop-shadow(0 4px 20px rgba(0,0,0,0.4));
+ opacity: 0.95;
+ animation: nfc-logo-in 1.2s cubic-bezier(0.16, 1, 0.3, 1) both;
+}
+
+@keyframes nfc-logo-in {
+ from { opacity: 0; transform: translateX(-50%) translateY(-12px) scale(0.94); }
+ to { opacity: 0.95; transform: translateX(-50%) translateY(0) scale(1); }
+}
+
+.nfc-kiosk__company {
+ position: absolute;
+ top: 5.5rem;
+ left: 50%;
+ transform: translateX(-50%);
+ font-size: 0.95rem;
+ color: var(--nfc-text-muted);
+ letter-spacing: 0.05em;
+ text-transform: uppercase;
}
.nfc-kiosk__time {
position: absolute;
- top: 1.5rem;
+ top: 1.75rem;
right: 2rem;
- font-size: 2rem;
- font-weight: 600;
- color: $nfc-text;
+ font-size: 2.25rem;
+ font-weight: 300;
+ color: var(--nfc-text);
font-variant-numeric: tabular-nums;
+ letter-spacing: -0.02em;
+ text-shadow: 0 2px 12px rgba(0,0,0,0.3);
}
.nfc-kiosk__date {
position: absolute;
- top: 4.5rem;
+ top: 5rem;
right: 2rem;
- font-size: 1rem;
- color: $nfc-text-muted;
+ font-size: 0.9rem;
+ color: var(--nfc-text-muted);
+ letter-spacing: 0.02em;
}
.nfc-kiosk__location {
position: absolute;
bottom: 1.5rem;
- left: 2rem;
- font-size: 0.95rem;
- color: $nfc-text-muted;
+ left: 1.5rem;
+ font-size: 0.85rem;
+ color: var(--nfc-text-muted);
+ background: rgba(255,255,255,0.04);
+ backdrop-filter: blur(20px);
+ -webkit-backdrop-filter: blur(20px);
+ border: 1px solid rgba(255,255,255,0.08);
+ padding: 0.5rem 1rem;
+ border-radius: 999px;
}
.nfc-kiosk__settings {
position: absolute;
- bottom: 1rem;
- right: 1rem;
- width: 2.5rem;
- height: 2.5rem;
+ bottom: 1.5rem;
+ right: 1.5rem;
+ width: 2.75rem;
+ height: 2.75rem;
border-radius: 50%;
- background: transparent;
- color: $nfc-text-muted;
- border: 1px solid $nfc-border;
+ background: rgba(255,255,255,0.04);
+ backdrop-filter: blur(20px);
+ -webkit-backdrop-filter: blur(20px);
+ color: var(--nfc-text-muted);
+ border: 1px solid rgba(255,255,255,0.08);
cursor: pointer;
font-size: 1.2rem;
display: flex;
align-items: center;
justify-content: center;
+ transition: color 200ms ease, border-color 200ms ease, transform 200ms ease;
- &:hover { color: $nfc-text; }
+ &:hover, &:active {
+ color: var(--nfc-text);
+ border-color: rgba(255,255,255,0.2);
+ transform: scale(1.05);
+ }
}
+// ─────────────────────────────────────────────────────────────────────
+// Reusable glass panel
+// ─────────────────────────────────────────────────────────────────────
+%nfc-glass {
+ background: rgba(255,255,255,0.05);
+ backdrop-filter: blur(24px) saturate(160%);
+ -webkit-backdrop-filter: blur(24px) saturate(160%);
+ border: 1px solid rgba(255,255,255,0.1);
+ box-shadow:
+ 0 20px 60px rgba(0,0,0,0.5),
+ inset 0 1px 0 rgba(255,255,255,0.08);
+ border-radius: 1.5rem;
+}
+
+// ─────────────────────────────────────────────────────────────────────
+// State container — base fade-in for whatever child renders
+// ─────────────────────────────────────────────────────────────────────
+#nfc_state_container > * {
+ animation: nfc-state-in 400ms cubic-bezier(0.16, 1, 0.3, 1);
+}
+
+@keyframes nfc-state-in {
+ from { opacity: 0; transform: scale(0.96) translateY(10px); }
+ to { opacity: 1; transform: scale(1) translateY(0); }
+}
+
+// ─────────────────────────────────────────────────────────────────────
+// IDLE — large NFC icon + prompt
+// ─────────────────────────────────────────────────────────────────────
.nfc-kiosk__idle {
text-align: center;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ gap: 2rem;
}
-.nfc-kiosk__icon {
- font-size: 8rem;
- color: $nfc-accent;
- animation: nfc-pulse 2s ease-in-out infinite;
+.nfc-kiosk__icon-svg {
+ width: 13rem;
+ height: 13rem;
+ color: hsl(var(--nfc-h), 80%, 65%);
+ filter: drop-shadow(0 0 30px hsla(var(--nfc-h), 80%, 55%, 0.6));
+
+ .nfc-chip {
+ animation: nfc-chip-pulse 2.5s ease-in-out infinite;
+ transform-origin: center;
+ }
+
+ .nfc-wave {
+ transform-origin: center;
+ opacity: 0;
+ animation: nfc-wave-emit 2.5s ease-out infinite;
+ }
+ .nfc-wave-1 { animation-delay: 0s; }
+ .nfc-wave-2 { animation-delay: 0.6s; }
+ .nfc-wave-3 { animation-delay: 1.2s; }
}
-@keyframes nfc-pulse {
- 0%, 100% { opacity: 0.85; transform: scale(1); }
- 50% { opacity: 1.0; transform: scale(1.06); }
+@keyframes nfc-chip-pulse {
+ 0%, 100% { transform: scale(1); }
+ 50% { transform: scale(1.06); }
+}
+
+@keyframes nfc-wave-emit {
+ 0% { transform: scale(0.5); opacity: 0; }
+ 20% { opacity: 0.8; }
+ 100% { transform: scale(1.4); opacity: 0; }
}
.nfc-kiosk__prompt {
- font-size: 2rem;
- font-weight: 500;
- margin-top: 2rem;
+ font-size: 2.25rem;
+ font-weight: 300;
+ letter-spacing: -0.01em;
+ color: var(--nfc-text);
+ text-shadow: 0 2px 20px rgba(0,0,0,0.4);
}
+// ─────────────────────────────────────────────────────────────────────
+// PROCESSING — pulsing dots
+// ─────────────────────────────────────────────────────────────────────
.nfc-kiosk__processing {
+ @extend %nfc-glass;
+ padding: 2.5rem 3.5rem;
text-align: center;
font-size: 1.5rem;
- color: $nfc-text-muted;
+ color: var(--nfc-text);
+ display: inline-flex;
+ align-items: center;
+ gap: 1rem;
+
+ .dots {
+ display: inline-flex;
+ gap: 0.4rem;
+
+ span {
+ width: 0.6rem;
+ height: 0.6rem;
+ border-radius: 50%;
+ background: hsl(var(--nfc-h), 80%, 65%);
+ animation: nfc-dot-bounce 1.2s ease-in-out infinite;
+
+ &:nth-child(2) { animation-delay: 0.15s; }
+ &:nth-child(3) { animation-delay: 0.3s; }
+ }
+ }
}
+@keyframes nfc-dot-bounce {
+ 0%, 80%, 100% { transform: scale(0.7); opacity: 0.5; }
+ 40% { transform: scale(1.0); opacity: 1.0; }
+}
+
+// ─────────────────────────────────────────────────────────────────────
+// RESULT — glass card with avatar + name + action
+// ─────────────────────────────────────────────────────────────────────
.nfc-kiosk__result {
+ @extend %nfc-glass;
width: 80vw;
- max-width: 700px;
+ max-width: 720px;
padding: 2.5rem 3rem;
- border-radius: 1rem;
display: flex;
align-items: center;
gap: 2rem;
+ position: relative;
- &--success { background: $nfc-success; }
- &--error { background: $nfc-error; }
+ &--success {
+ border-color: rgba(24,169,87,0.55);
+ box-shadow:
+ 0 20px 60px rgba(0,0,0,0.5),
+ 0 0 80px rgba(24,169,87,0.35),
+ inset 0 1px 0 rgba(255,255,255,0.1);
+ animation: nfc-success-burst 700ms cubic-bezier(0.16, 1, 0.3, 1);
+ }
+
+ &--error {
+ border-color: rgba(217,55,78,0.55);
+ box-shadow:
+ 0 20px 60px rgba(0,0,0,0.5),
+ 0 0 60px rgba(217,55,78,0.3),
+ inset 0 1px 0 rgba(255,255,255,0.1);
+ animation: nfc-shake 350ms ease-in-out, nfc-state-in 400ms cubic-bezier(0.16, 1, 0.3, 1);
+ }
+}
+
+@keyframes nfc-success-burst {
+ 0% {
+ opacity: 0;
+ transform: scale(0.88);
+ box-shadow:
+ 0 20px 60px rgba(0,0,0,0.5),
+ 0 0 0 rgba(24,169,87,0),
+ inset 0 1px 0 rgba(255,255,255,0.1);
+ }
+ 50% {
+ box-shadow:
+ 0 20px 60px rgba(0,0,0,0.5),
+ 0 0 140px rgba(24,169,87,0.7),
+ inset 0 1px 0 rgba(255,255,255,0.1);
+ }
+ 100% {
+ opacity: 1;
+ transform: scale(1);
+ box-shadow:
+ 0 20px 60px rgba(0,0,0,0.5),
+ 0 0 80px rgba(24,169,87,0.35),
+ inset 0 1px 0 rgba(255,255,255,0.1);
+ }
+}
+
+@keyframes nfc-shake {
+ 0%, 100% { transform: translateX(0); }
+ 20% { transform: translateX(-10px); }
+ 40% { transform: translateX(10px); }
+ 60% { transform: translateX(-6px); }
+ 80% { transform: translateX(6px); }
}
.nfc-kiosk__avatar {
@@ -138,53 +372,91 @@ html, body {
border-radius: 50%;
background-size: cover;
background-position: center;
- background-color: rgba(255,255,255,0.2);
+ background-color: rgba(255,255,255,0.15);
flex-shrink: 0;
+ border: 2px solid rgba(255,255,255,0.2);
+ box-shadow: 0 8px 24px rgba(0,0,0,0.4);
+ animation: nfc-avatar-in 600ms cubic-bezier(0.34, 1.56, 0.64, 1) both;
+}
+
+@keyframes nfc-avatar-in {
+ from { opacity: 0; transform: scale(0.4); }
+ to { opacity: 1; transform: scale(1); }
}
.nfc-kiosk__result-text {
flex: 1;
- .name { font-size: 2rem; font-weight: 700; }
- .action { font-size: 1.5rem; margin-top: 0.5rem; }
- .hours { font-size: 1.1rem; opacity: 0.9; margin-top: 0.25rem; }
+ .name { font-size: 2.25rem; font-weight: 600; letter-spacing: -0.02em; }
+ .action { font-size: 1.5rem; margin-top: 0.5rem; opacity: 0.95; font-weight: 400; }
+ .hours { font-size: 1.05rem; opacity: 0.75; margin-top: 0.5rem; }
}
+// ─────────────────────────────────────────────────────────────────────
+// SETUP wizard
+// ─────────────────────────────────────────────────────────────────────
.nfc-kiosk__setup {
+ @extend %nfc-glass;
text-align: center;
max-width: 600px;
+ padding: 3.5rem 3rem;
- h2 { font-size: 2rem; margin-bottom: 1rem; }
- p { color: $nfc-text-muted; margin-bottom: 2rem; }
+ h2 { font-size: 2rem; margin-bottom: 1rem; font-weight: 300; letter-spacing: -0.01em; }
+ p { color: var(--nfc-text-muted); margin-bottom: 2rem; line-height: 1.5; }
button {
- font-size: 1.5rem;
- padding: 1rem 3rem;
- background: $nfc-accent;
+ font-size: 1.25rem;
+ padding: 1rem 2.5rem;
+ background: hsl(var(--nfc-h), 80%, 55%);
color: white;
border: none;
- border-radius: 0.5rem;
+ border-radius: 999px;
cursor: pointer;
+ font-weight: 500;
+ box-shadow: 0 8px 32px hsla(var(--nfc-h), 80%, 50%, 0.4);
+ transition: transform 200ms ease, box-shadow 200ms ease;
+
+ &:hover, &:active {
+ transform: translateY(-2px);
+ box-shadow: 0 12px 36px hsla(var(--nfc-h), 80%, 50%, 0.5);
+ }
}
}
+// ─────────────────────────────────────────────────────────────────────
+// ENROLL Mode overlay — glass panel with numpad / search / tap-prompt
+// ─────────────────────────────────────────────────────────────────────
.nfc-kiosk__enroll-overlay {
- position: fixed; inset: 0;
- background: rgba(0,0,0,0.85);
+ position: fixed;
+ inset: 0;
+ background: rgba(0,0,0,0.7);
+ backdrop-filter: blur(8px);
+ -webkit-backdrop-filter: blur(8px);
z-index: 1000;
- display: flex; align-items: center; justify-content: center;
+ display: flex;
+ align-items: center;
+ justify-content: center;
padding: 2rem;
+ animation: nfc-overlay-in 250ms ease-out;
+}
+
+@keyframes nfc-overlay-in {
+ from { opacity: 0; }
+ to { opacity: 1; }
}
.nfc-kiosk__enroll-panel {
- background: $nfc-panel;
- border: 1px solid $nfc-border;
- border-radius: 1rem;
+ @extend %nfc-glass;
padding: 2.5rem;
width: 80vw;
- max-width: 700px;
+ max-width: 720px;
- h2 { font-size: 1.5rem; margin: 0 0 1.5rem; }
+ h2 {
+ font-size: 1.5rem;
+ margin: 0 0 1.5rem;
+ font-weight: 400;
+ color: var(--nfc-text);
+ }
.numpad {
display: grid;
@@ -193,27 +465,45 @@ html, body {
margin: 1rem 0;
button {
- font-size: 2rem; padding: 1.5rem 0;
- background: $nfc-bg; color: $nfc-text;
- border: 1px solid $nfc-border; border-radius: 0.5rem;
+ font-size: 2rem;
+ padding: 1.5rem 0;
+ background: rgba(255,255,255,0.05);
+ color: var(--nfc-text);
+ border: 1px solid rgba(255,255,255,0.1);
+ border-radius: 0.75rem;
cursor: pointer;
+ transition: background 150ms ease, transform 100ms ease;
+ font-weight: 300;
+
+ &:hover { background: rgba(255,255,255,0.1); }
+ &:active { background: rgba(255,255,255,0.15); transform: scale(0.96); }
}
}
.pin-display {
- font-size: 2.5rem; letter-spacing: 0.5rem;
- text-align: center; margin: 1rem 0;
+ font-size: 2.5rem;
+ letter-spacing: 0.5rem;
+ text-align: center;
+ margin: 1rem 0 1.5rem;
font-variant-numeric: tabular-nums;
+ min-height: 3rem;
+ color: hsl(var(--nfc-h), 80%, 70%);
}
.employee-search {
width: 100%;
- padding: 0.75rem 1rem;
- font-size: 1.25rem;
- background: $nfc-bg; color: $nfc-text;
- border: 1px solid $nfc-border;
- border-radius: 0.5rem;
+ padding: 1rem 1.25rem;
+ font-size: 1.15rem;
+ background: rgba(255,255,255,0.05);
+ color: var(--nfc-text);
+ border: 1px solid rgba(255,255,255,0.1);
+ border-radius: 0.75rem;
margin-bottom: 1rem;
+ box-sizing: border-box;
+ outline: none;
+
+ &:focus { border-color: hsl(var(--nfc-h), 80%, 55%); }
+ &::placeholder { color: var(--nfc-text-muted); }
}
.employee-list {
@@ -221,24 +511,53 @@ html, body {
overflow-y: auto;
.employee-row {
- padding: 0.75rem 1rem;
- border-bottom: 1px solid $nfc-border;
+ padding: 0.85rem 1rem;
+ border-bottom: 1px solid rgba(255,255,255,0.05);
cursor: pointer;
- font-size: 1.1rem;
+ font-size: 1.05rem;
+ border-radius: 0.5rem;
+ transition: background 150ms ease;
- &:hover { background: $nfc-bg; }
+ &:hover, &:active { background: rgba(255,255,255,0.06); }
}
}
.actions {
- display: flex; gap: 1rem; justify-content: flex-end;
+ display: flex;
+ gap: 1rem;
+ justify-content: flex-end;
margin-top: 1.5rem;
button {
- font-size: 1rem; padding: 0.75rem 1.5rem;
- border-radius: 0.5rem; cursor: pointer; border: none;
+ font-size: 1rem;
+ padding: 0.85rem 1.75rem;
+ border-radius: 999px;
+ cursor: pointer;
+ border: none;
+ font-weight: 500;
+ transition: transform 150ms ease, opacity 150ms ease;
+
+ &:hover, &:active { transform: scale(0.97); }
+ }
+ .cancel { background: rgba(255,255,255,0.08); color: var(--nfc-text); }
+ .confirm {
+ background: hsl(var(--nfc-h), 80%, 55%);
+ color: white;
+ box-shadow: 0 4px 20px hsla(var(--nfc-h), 80%, 50%, 0.4);
}
- .cancel { background: $nfc-border; color: $nfc-text; }
- .confirm { background: $nfc-accent; color: white; }
}
}
+
+// ─────────────────────────────────────────────────────────────────────
+// Reduced-motion fallback — respect users who prefer no animation
+// ─────────────────────────────────────────────────────────────────────
+@media (prefers-reduced-motion: reduce) {
+ .nfc-kiosk::before { animation: none; }
+ .nfc-kiosk__icon-svg .nfc-wave,
+ .nfc-kiosk__icon-svg .nfc-chip,
+ #nfc_state_container > *,
+ .nfc-kiosk__logo,
+ .nfc-kiosk__result--success,
+ .nfc-kiosk__result--error,
+ .nfc-kiosk__avatar { animation: none; }
+}
diff --git a/fusion_clock/views/kiosk_nfc_templates.xml b/fusion_clock/views/kiosk_nfc_templates.xml
index 500e3225..1421a3da 100644
--- a/fusion_clock/views/kiosk_nfc_templates.xml
+++ b/fusion_clock/views/kiosk_nfc_templates.xml
@@ -12,7 +12,16 @@
+ t-att-data-location-configured="'1' if location_configured else '0'"
+ t-att-data-company-logo-url="company_logo_url or ''">
+
+
+
@@ -24,9 +33,9 @@
-
+
-
+
Welcome to Fusion Clock NFC Kiosk
Tap the button below to enable the NFC reader and camera. This is a one-time setup for this device.
@@ -34,7 +43,7 @@
-
+