From 3e03f1b9aa6994a1f484363fef376a55aeb0ce42 Mon Sep 17 00:00:00 2001 From: David Nguyen Date: Sun, 4 Oct 2026 16:05:50 +0700 Subject: [PATCH 01/27] feat: add Arcade FAQ chatbot --- components/arcade/arcade-faq-chatbot.tsx | 498 +++++++++++++++++++++++ 1 file changed, 498 insertions(+) create mode 100644 components/arcade/arcade-faq-chatbot.tsx diff --git a/components/arcade/arcade-faq-chatbot.tsx b/components/arcade/arcade-faq-chatbot.tsx new file mode 100644 index 000000000..4177d20a0 --- /dev/null +++ b/components/arcade/arcade-faq-chatbot.tsx @@ -0,0 +1,498 @@ +"use client" + +import { + ExternalLink, + MessageCircle, + Send, + Sparkles, + X, +} from "lucide-react" +import type { FormEvent } from "react" +import { useEffect, useMemo, useRef, useState } from "react" + +type ChatSource = { + label: string + href: string +} + +type FaqItem = { + id: string + keywords: string[] + answer: { + en: string + vi: string + } + sources: ChatSource[] +} + +type ChatMessage = { + id: number + role: "assistant" | "user" + text: string + sources?: ChatSource[] +} + +const TIER_SOURCE = + "https://discuss.google.dev/t/google-skills-arcade-2026-tiers/371066" +const PRIZE_SOURCE = + "https://discuss.google.dev/t/clarification-your-2026-arcade-points-the-prize-counter/349716" +const ARCADE_OVERVIEW_SOURCE = + "https://cloud.google.com/blog/topics/training-certifications/the-arcade-with-google-cloud-game-helps-boost-cloud-skills" +const SPECIAL_GAME_SOURCE = + "https://discuss.google.dev/t/chasing-the-win-with-pup-kit/331876" + +const FAQ_ITEMS: FaqItem[] = [ + { + id: "tiers", + keywords: [ + "tier", + "trooper", + "ranger", + "champion", + "legend", + "moc diem", + "hang", + "cap bac", + "bao nhieu diem de", + ], + answer: { + en: "For the 2026 season, the official prize tiers are: Trooper 50–74 points, Ranger 75–94, Champion 95–119, and Legend 120+. Prize capacity is limited to 6,000 / 4,000 / 3,000 / 2,500 slots respectively, and Google uses a first-come-first-served waterfall system when a higher tier fills.", + vi: "Mùa 2026 có 4 tier thưởng chính thức: Trooper 50–74 điểm, Ranger 75–94, Champion 95–119 và Legend từ 120 điểm. Số suất lần lượt là 6.000 / 4.000 / 3.000 / 2.500; Google áp dụng cơ chế first-come-first-served và waterfall khi tier cao đã đầy.", + }, + sources: [{ label: "Official 2026 tier announcement", href: TIER_SOURCE }], + }, + { + id: "prize-counter", + keywords: [ + "prize counter", + "redeem", + "reward", + "swag", + "qua", + "doi qua", + "nhan qua", + "trao thuong", + "khi nao co qua", + "khi nao doi", + "prize", + ], + answer: { + en: "For 2026, Google moved to one unified Prize Counter window after the season wraps up at the end of the year. Google has confirmed the window is coming, but an exact opening date should only be trusted once it is officially announced. Your 2026 points remain counted toward that redemption window.", + vi: "Năm 2026 Google chuyển sang một đợt Prize Counter duy nhất sau khi mùa Arcade kết thúc vào cuối năm. Google đã xác nhận Prize Counter chắc chắn sẽ mở, nhưng ngày mở chính xác chỉ nên tin khi có thông báo chính thức. Điểm bạn kiếm trong năm 2026 vẫn được tính cho đợt đổi quà này.", + }, + sources: [ + { label: "2026 Prize Counter clarification", href: PRIZE_SOURCE }, + { label: "2026 tiers & season timing", href: TIER_SOURCE }, + ], + }, + { + id: "lab-release", + keywords: [ + "lab", + "game moi", + "new game", + "release", + "released", + "ra lab", + "mo lab", + "lich lab", + "khi nao co lab", + "deadline", + "monthly lab", + ], + answer: { + en: "Arcade activities are time-limited, but Google does not guarantee one fixed global release hour for every monthly or special game. Monthly games and special events can use different start/end dates. For the most practical view, check this site's Monthly Labs page for known access codes/deadlines, then verify the final timing on the official Arcade page before starting.", + vi: "Các hoạt động Arcade có thời hạn nhưng Google không cam kết một giờ mở cố định cho mọi monthly game hoặc special game. Mỗi game có thể có ngày bắt đầu/kết thúc khác nhau. Bạn nên xem trang Monthly Labs trên site này để biết access code/deadline đã thu thập được, sau đó kiểm tra lại trang Arcade chính thức trước khi làm.", + }, + sources: [ + { label: "Google Cloud Arcade overview", href: ARCADE_OVERVIEW_SOURCE }, + ], + }, + { + id: "points", + keywords: [ + "point", + "points", + "score", + "diem", + "tinh diem", + "cach tinh", + "skill badge", + "game badge", + "trivia", + "bao nhieu diem", + ], + answer: { + en: "Arcade points come from eligible completed badges/activities, not from each individual lab task. This calculator follows the current scoring model used by the project: normal Game/Trivia badges are typically 1 point, and 2 Skill Badges equal 1 Arcade point. Special games can override the normal value — some 2026 special games awarded 3 points — so the official game page and Arcade Insider total are the final authority.", + vi: "Arcade Point được tính từ badge/hoạt động đủ điều kiện đã hoàn thành, không phải mỗi task nhỏ trong lab. Calculator hiện dùng rule: Game/Trivia thông thường thường là 1 điểm và 2 Skill Badge = 1 Arcade Point. Special Game có thể có điểm riêng — đã có special game năm 2026 cho 3 điểm — nên trang game chính thức và tổng điểm trong Arcade Insider vẫn là nguồn quyết định cuối cùng.", + }, + sources: [ + { label: "Google Cloud Arcade overview", href: ARCADE_OVERVIEW_SOURCE }, + { label: "2026 special-game example", href: SPECIAL_GAME_SOURCE }, + ], + }, + { + id: "waterfall", + keywords: [ + "waterfall", + "het slot", + "het suat", + "full tier", + "slot", + "spots left", + "con suat", + "first come", + ], + answer: { + en: "In 2026, prize eligibility and point tier are not exactly the same thing because each tier has limited slots. If a higher-tier pool fills, eligible players can roll down to the next tier under Google's waterfall system. Reaching the point threshold therefore does not guarantee an item from that exact tier.", + vi: "Năm 2026, đạt mốc điểm chưa đồng nghĩa chắc chắn nhận quà đúng tier vì mỗi tier có số suất giới hạn. Nếu tier cao đã đầy, người đủ điều kiện có thể được chuyển xuống tier kế tiếp theo cơ chế waterfall. Vì vậy đạt đủ điểm không đảm bảo 100% còn suất quà ở tier đó.", + }, + sources: [{ label: "Official waterfall rules", href: TIER_SOURCE }], + }, + { + id: "shipping", + keywords: [ + "ship", + "shipping", + "delivery", + "giao hang", + "giao qua", + "bao lau nhan", + "nhan hang", + "van chuyen", + ], + answer: { + en: "Google has said the 2026 fulfillment process is being changed to shorten the wait after redemption, but there is no universal delivery-time guarantee for every country. After you redeem, follow the confirmation email and carrier tracking; treat any community ETA as an estimate unless Google gives a specific date.", + vi: "Google cho biết quy trình fulfillment 2026 đang được thay đổi để rút ngắn thời gian chờ sau khi đổi quà, nhưng không có một SLA giao hàng cố định cho mọi quốc gia. Sau khi redeem, hãy theo dõi email xác nhận và tracking của đơn vị vận chuyển; các mốc thời gian từ cộng đồng chỉ nên xem là ước tính.", + }, + sources: [{ label: "2026 fulfillment update", href: PRIZE_SOURCE }], + }, + { + id: "subscribe", + keywords: [ + "subscribe", + "subscription", + "register", + "dang ky", + "tham gia", + "bat dau", + "start arcade", + "arcade la gi", + "what is arcade", + ], + answer: { + en: "Google Skills Arcade is a hands-on learning program built around limited-time games and challenges on Google Skills. You complete cloud labs, earn badges, build Arcade points, and may qualify for rewards. Use the official Arcade page for enrollment and current event access; this site is an unofficial tracker/calculator.", + vi: "Google Skills Arcade là chương trình học thực hành trên Google Skills với các game/challenge có thời hạn. Bạn làm lab Cloud, nhận badge, tích Arcade Point và có thể đủ điều kiện nhận reward. Việc đăng ký và truy cập sự kiện nên thực hiện trên trang Arcade chính thức; site này là công cụ theo dõi/calculator không chính thức.", + }, + sources: [{ label: "Google Cloud Arcade overview", href: ARCADE_OVERVIEW_SOURCE }], + }, + { + id: "missing-points", + keywords: [ + "missing point", + "missing points", + "wrong point", + "sai diem", + "thieu diem", + "chua cong diem", + "khong thay diem", + "insider", + ], + answer: { + en: "If your estimate and Google's total differ, treat Google's Arcade Insider / official Arcade total as authoritative. First verify that the expected badge is actually earned on your public Google Skills profile and that the activity belongs to the current scoring window. This site's result is an estimate built from public badge data.", + vi: "Nếu điểm ước tính khác số Google gửi, hãy ưu tiên tổng điểm trong Arcade Insider / nguồn chính thức của Google. Trước tiên kiểm tra badge đã thực sự xuất hiện trên public Google Skills profile và hoạt động đó nằm trong thời gian được tính điểm. Kết quả trên site này chỉ là ước tính từ dữ liệu badge công khai.", + }, + sources: [{ label: "Official 2026 tracking guidance", href: TIER_SOURCE }], + }, +] + +const QUICK_PROMPTS = { + en: [ + "How are Arcade points calculated?", + "What are the 2026 prize tiers?", + "When does the Prize Counter open?", + "When are new labs released?", + ], + vi: [ + "Arcade tính điểm như thế nào?", + "Tier thưởng 2026 gồm những mốc nào?", + "Khi nào Prize Counter mở?", + "Khi nào có lab mới?", + ], +} + +function normalize(value: string): string { + return value + .toLocaleLowerCase() + .normalize("NFD") + .replace(/[\u0300-\u036f]/g, "") + .replace(/đ/g, "d") + .replace(/[^a-z0-9+\s-]/g, " ") + .replace(/\s+/g, " ") + .trim() +} + +function detectVietnamese(value: string, locale: string): boolean { + if (locale.startsWith("vi")) return true + if (/[ăâđêôơưà-ỹ]/i.test(value)) return true + const normalized = normalize(value) + return /\b(khi nao|diem|qua|lab moi|tinh diem|dang ky|nhan hang|bao lau|cach)\b/.test( + normalized, + ) +} + +function findAnswer(question: string): FaqItem | null { + const normalized = normalize(question) + if (!normalized) return null + + let best: { item: FaqItem; score: number } | null = null + for (const item of FAQ_ITEMS) { + const score = item.keywords.reduce( + (total, keyword) => total + (normalized.includes(normalize(keyword)) ? 1 : 0), + 0, + ) + if (score > 0 && (!best || score > best.score)) { + best = { item, score } + } + } + return best?.item ?? null +} + +function getLocale(): string { + if (typeof document === "undefined") return "en" + return ( + document.documentElement.dataset.locale || + document.documentElement.lang || + "en" + ).toLowerCase() +} + +export default function ArcadeFaqChatbot() { + const [open, setOpen] = useState(false) + const [input, setInput] = useState("") + const [locale, setLocale] = useState("en") + const [hiddenForWidget, setHiddenForWidget] = useState(false) + const [messages, setMessages] = useState([]) + const messageId = useRef(0) + const logRef = useRef(null) + const inputRef = useRef(null) + + const vietnameseUi = locale.startsWith("vi") + const prompts = vietnameseUi ? QUICK_PROMPTS.vi : QUICK_PROMPTS.en + + const welcome = useMemo( + () => + vietnameseUi + ? "Chào bạn! Mình có thể trả lời nhanh về Google Skills Arcade 2026: cách tính điểm, tier, lab/game, Prize Counter và reward." + : "Hi! I can answer quick questions about Google Skills Arcade 2026: points, tiers, labs/games, the Prize Counter and rewards.", + [vietnameseUi], + ) + + useEffect(() => { + setLocale(getLocale()) + setHiddenForWidget( + /^\/(?:[a-z]{2}(?:-[a-z]{2})?\/)?widget\/?$/i.test( + window.location.pathname, + ), + ) + + const observer = new MutationObserver(() => setLocale(getLocale())) + observer.observe(document.documentElement, { + attributes: true, + attributeFilter: ["lang", "data-locale"], + }) + return () => observer.disconnect() + }, []) + + useEffect(() => { + if (!open) return + + const onEscape = (event: KeyboardEvent) => { + if (event.key === "Escape") setOpen(false) + } + document.addEventListener("keydown", onEscape) + window.setTimeout(() => inputRef.current?.focus(), 0) + return () => document.removeEventListener("keydown", onEscape) + }, [open]) + + useEffect(() => { + if (!open) return + logRef.current?.scrollTo({ + top: logRef.current.scrollHeight, + behavior: "smooth", + }) + }, [messages, open]) + + function ask(rawQuestion: string) { + const question = rawQuestion.trim() + if (!question) return + + const useVietnamese = detectVietnamese(question, locale) + const matched = findAnswer(question) + const fallback = useVietnamese + ? "Mình chưa có câu trả lời chắc chắn cho câu này. Bạn có thể hỏi về cách tính điểm, tier 2026, lịch lab/game, Prize Counter, shipping hoặc điểm bị thiếu. Với thông tin có thể thay đổi, hãy ưu tiên thông báo chính thức của Google." + : "I do not have a reliable answer for that yet. Try asking about point calculation, 2026 tiers, lab/game timing, the Prize Counter, shipping, or missing points. For anything time-sensitive, prefer Google's official announcements." + + const nextUserId = ++messageId.current + const nextAssistantId = ++messageId.current + setMessages((current) => [ + ...current, + { id: nextUserId, role: "user", text: question }, + { + id: nextAssistantId, + role: "assistant", + text: matched + ? matched.answer[useVietnamese ? "vi" : "en"] + : fallback, + sources: matched?.sources, + }, + ]) + setInput("") + } + + function submit(event: FormEvent) { + event.preventDefault() + ask(input) + } + + if (hiddenForWidget) return null + + return ( +
+ {open ? ( +
+
+ +
+ Arcade Guide + + {vietnameseUi + ? "FAQ nhanh · cập nhật 2026" + : "Quick FAQ · 2026 guidance"} + +
+ +
+ +
+
+ +
+

{welcome}

+ + {vietnameseUi + ? "Thông tin có thể thay đổi; Google là nguồn quyết định cuối cùng." + : "Details can change; Google remains the final authority."} + +
+
+ + {messages.map((message) => ( +
+ {message.role === "assistant" ? ( + + ) : null} +
+

{message.text}

+ {message.sources && message.sources.length > 0 ? ( +
+ {message.sources.map((source) => ( + + {source.label} + + ))} +
+ ) : null} +
+
+ ))} + + {messages.length === 0 ? ( +
+ {prompts.map((prompt) => ( + + ))} +
+ ) : null} +
+ + + +
+ setInput(event.target.value)} + maxLength={240} + placeholder={ + vietnameseUi + ? "Hỏi về điểm, lab, reward..." + : "Ask about points, labs, rewards..." + } + aria-label={vietnameseUi ? "Nhập câu hỏi" : "Ask a question"} + /> + +
+
+ ) : null} + + +
+ ) +} From 8ff2beca12fc6c0e4e951519394565849acc7434 Mon Sep 17 00:00:00 2001 From: David Nguyen Date: Sun, 4 Oct 2026 16:05:53 +0700 Subject: [PATCH 02/27] style: add responsive Arcade chatbot UI --- app/styles/arcade-chatbot.css | 351 ++++++++++++++++++++++++++++++++++ 1 file changed, 351 insertions(+) create mode 100644 app/styles/arcade-chatbot.css diff --git a/app/styles/arcade-chatbot.css b/app/styles/arcade-chatbot.css new file mode 100644 index 000000000..a0f0ece84 --- /dev/null +++ b/app/styles/arcade-chatbot.css @@ -0,0 +1,351 @@ +.arcade-chatbot { + position: fixed; + right: 22px; + bottom: 22px; + z-index: 100; + display: grid; + justify-items: end; + gap: 12px; + font-family: var(--font-arcade-body), Inter, ui-sans-serif, system-ui, sans-serif; +} + +.arcade-chatbot-launcher { + display: inline-flex; + min-height: 48px; + align-items: center; + justify-content: center; + gap: 9px; + padding: 0 16px; + border: 1px solid var(--ui-accent-border); + border-radius: 999px; + background: var(--ui-primary); + color: var(--ui-on-accent); + box-shadow: 0 14px 34px hsl(224 50% 3% / .28); + font-size: .78rem; + font-weight: 800; + cursor: pointer; + transition: transform .16s ease, box-shadow .16s ease, filter .16s ease; +} + +.arcade-chatbot-launcher:hover { + transform: translateY(-2px); + filter: brightness(1.05); + box-shadow: 0 18px 42px hsl(224 50% 3% / .34); +} + +.arcade-chatbot-launcher svg { + width: 18px; + height: 18px; +} + +.arcade-chatbot-panel { + display: grid; + grid-template-rows: auto minmax(0, 1fr) auto auto; + width: min(390px, calc(100vw - 32px)); + height: min(570px, calc(100dvh - 112px)); + overflow: hidden; + border: 1px solid var(--ui-border); + border-radius: var(--ui-radius-lg); + background: var(--ui-surface); + color: var(--ui-text); + box-shadow: 0 24px 70px hsl(224 50% 3% / .42); +} + +.arcade-chatbot-header { + display: grid; + grid-template-columns: 40px minmax(0, 1fr) 36px; + align-items: center; + gap: 10px; + padding: 13px 14px; + border-bottom: 1px solid var(--ui-border); + background: + linear-gradient(135deg, hsl(var(--primary) / .14), transparent 65%), + var(--ui-surface); +} + +.arcade-chatbot-avatar, +.arcade-chatbot-mini-avatar { + display: grid; + place-items: center; + flex: 0 0 auto; + border: 1px solid var(--ui-accent-border); + background: var(--ui-accent-soft); + color: var(--ui-accent); +} + +.arcade-chatbot-avatar { + width: 40px; + height: 40px; + border-radius: 12px; +} + +.arcade-chatbot-avatar svg { + width: 18px; + height: 18px; +} + +.arcade-chatbot-header > div { + min-width: 0; +} + +.arcade-chatbot-header strong, +.arcade-chatbot-header span { + display: block; +} + +.arcade-chatbot-header strong { + color: var(--ui-text); + font-size: .86rem; + line-height: 1.25; +} + +.arcade-chatbot-header span { + margin-top: 3px; + color: var(--ui-muted); + font-size: .66rem; +} + +.arcade-chatbot-header > button { + display: grid; + place-items: center; + width: 34px; + height: 34px; + padding: 0; + border: 1px solid transparent; + border-radius: var(--ui-radius-sm); + background: transparent; + color: var(--ui-muted); + cursor: pointer; +} + +.arcade-chatbot-header > button:hover { + border-color: var(--ui-border); + background: var(--ui-soft); + color: var(--ui-text); +} + +.arcade-chatbot-header > button svg { + width: 16px; + height: 16px; +} + +.arcade-chatbot-log { + min-height: 0; + overflow-y: auto; + padding: 14px; + scroll-behavior: smooth; +} + +.arcade-chatbot-message { + display: flex; + align-items: flex-start; + gap: 8px; + margin-bottom: 12px; +} + +.arcade-chatbot-message > div { + max-width: 88%; + padding: 10px 11px; + border: 1px solid var(--ui-border); + border-radius: 5px 12px 12px; + background: var(--ui-soft); +} + +.arcade-chatbot-message.is-user { + justify-content: flex-end; +} + +.arcade-chatbot-message.is-user > div { + border-color: var(--ui-accent-border); + border-radius: 12px 5px 12px 12px; + background: var(--ui-accent-soft); +} + +.arcade-chatbot-message p { + margin: 0; + color: var(--ui-text); + font-size: .73rem; + line-height: 1.58; +} + +.arcade-chatbot-message small { + display: block; + margin-top: 7px; + color: var(--ui-muted); + font-size: .61rem; + line-height: 1.45; +} + +.arcade-chatbot-mini-avatar { + width: 25px; + height: 25px; + margin-top: 2px; + border-radius: 8px; +} + +.arcade-chatbot-mini-avatar svg { + width: 12px; + height: 12px; +} + +.arcade-chatbot-sources { + display: grid; + gap: 5px; + margin-top: 8px; + padding-top: 8px; + border-top: 1px solid var(--ui-border); +} + +.arcade-chatbot-sources a, +.arcade-chatbot-shortcuts a { + display: inline-flex; + align-items: center; + gap: 5px; + color: var(--ui-accent); + font-size: .61rem; + font-weight: 700; + text-decoration: none; +} + +.arcade-chatbot-sources a:hover, +.arcade-chatbot-shortcuts a:hover { + text-decoration: underline; + text-underline-offset: 3px; +} + +.arcade-chatbot-sources svg, +.arcade-chatbot-shortcuts svg { + width: 11px; + height: 11px; +} + +.arcade-chatbot-prompts { + display: grid; + gap: 7px; + margin: 4px 0 2px 33px; +} + +.arcade-chatbot-prompts button { + min-height: 38px; + padding: 8px 10px; + border: 1px solid var(--ui-border); + border-radius: var(--ui-radius-sm); + background: var(--ui-surface); + color: var(--ui-muted); + font-size: .68rem; + font-weight: 650; + text-align: left; + cursor: pointer; +} + +.arcade-chatbot-prompts button:hover { + border-color: var(--ui-accent-border); + background: var(--ui-accent-soft); + color: var(--ui-text); +} + +.arcade-chatbot-shortcuts { + display: flex; + flex-wrap: wrap; + gap: 8px 14px; + padding: 9px 14px; + border-top: 1px solid var(--ui-border); + background: var(--ui-soft); +} + +.arcade-chatbot-form { + display: grid; + grid-template-columns: minmax(0, 1fr) 40px; + gap: 8px; + padding: 10px; + border-top: 1px solid var(--ui-border); + background: var(--ui-surface); +} + +.arcade-chatbot-form input { + width: 100%; + min-width: 0; + min-height: 40px; + padding: 0 11px; + border: 1px solid var(--ui-border); + border-radius: var(--ui-radius-sm); + outline: 0; + background: var(--ui-soft); + color: var(--ui-text); + font-size: .72rem; +} + +.arcade-chatbot-form input::placeholder { + color: var(--ui-muted); +} + +.arcade-chatbot-form input:focus { + border-color: var(--ui-accent-border); + box-shadow: 0 0 0 3px hsl(var(--primary) / .12); +} + +.arcade-chatbot-form button { + display: grid; + place-items: center; + width: 40px; + min-height: 40px; + padding: 0; + border: 0; + border-radius: var(--ui-radius-sm); + background: var(--ui-primary); + color: var(--ui-on-accent); + cursor: pointer; +} + +.arcade-chatbot-form button:disabled { + opacity: .45; +} + +.arcade-chatbot-form button svg { + width: 15px; + height: 15px; +} + +@media (max-width: 640px) { + .arcade-chatbot { + right: 12px; + bottom: 84px; + } + + .arcade-chatbot-panel { + width: calc(100vw - 24px); + height: min(540px, calc(100dvh - 164px)); + border-radius: 14px; + } + + .arcade-chatbot-launcher { + min-height: 46px; + padding: 0 14px; + } +} + +@media (max-width: 420px) { + .arcade-chatbot-panel { + height: min(520px, calc(100dvh - 156px)); + } + + .arcade-chatbot-log { + padding: 12px; + } + + .arcade-chatbot-message > div { + max-width: 90%; + } + + .arcade-chatbot-prompts { + margin-left: 31px; + } +} + +@media (prefers-reduced-motion: reduce) { + .arcade-chatbot-launcher, + .arcade-chatbot-log { + transition: none; + scroll-behavior: auto; + } +} From c2f5c753f6c5dcef885c07f1a55813809c0575dd Mon Sep 17 00:00:00 2001 From: David Nguyen Date: Sun, 4 Oct 2026 16:05:56 +0700 Subject: [PATCH 03/27] test: cover Arcade chatbot integration --- tests/arcade-chatbot.test.mjs | 38 +++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 tests/arcade-chatbot.test.mjs diff --git a/tests/arcade-chatbot.test.mjs b/tests/arcade-chatbot.test.mjs new file mode 100644 index 000000000..938df3cc0 --- /dev/null +++ b/tests/arcade-chatbot.test.mjs @@ -0,0 +1,38 @@ +import test from "node:test" +import assert from "node:assert/strict" +import { readFileSync } from "node:fs" + +const chatbot = readFileSync("components/arcade/arcade-faq-chatbot.tsx", "utf8") +const chatbotCss = readFileSync("app/styles/arcade-chatbot.css", "utf8") +const layout = readFileSync("app/layout.tsx", "utf8") + +test("Arcade FAQ chatbot is mounted globally", () => { + assert.match(layout, /ArcadeFaqChatbot/) + assert.match(layout, /arcade-chatbot\.css/) + assert.match(layout, //) +}) + +test("Arcade FAQ chatbot includes verified 2026 tier guidance", () => { + assert.match(chatbot, /Trooper 50–74/) + assert.match(chatbot, /Ranger 75–94/) + assert.match(chatbot, /Champion 95–119/) + assert.match(chatbot, /Legend 120\+/) + assert.match(chatbot, /waterfall/i) + assert.match(chatbot, /6,000/) + assert.match(chatbot, /2,500/) +}) + +test("Arcade FAQ chatbot covers core user questions without an AI API dependency", () => { + assert.match(chatbot, /2 Skill Badges equal 1 Arcade point/) + assert.match(chatbot, /Prize Counter/) + assert.match(chatbot, /Monthly Labs/) + assert.match(chatbot, /shipping/i) + assert.doesNotMatch(chatbot, /\bfetch\s*\(/) + assert.doesNotMatch(chatbot, /OPENAI_API_KEY|GEMINI_API_KEY/) +}) + +test("Arcade FAQ chatbot is responsive and avoids the mobile bottom navigation", () => { + assert.match(chatbotCss, /@media \(max-width: 640px\)/) + assert.match(chatbotCss, /bottom: 84px/) + assert.match(chatbotCss, /100dvh/) +}) From 5e9aae77c6e6018db33211e9fbfc3a3d7745378b Mon Sep 17 00:00:00 2001 From: David Nguyen Date: Sun, 4 Oct 2026 16:05:58 +0700 Subject: [PATCH 04/27] feat: mount Arcade FAQ chatbot globally --- app/layout.tsx | 3 +++ 1 file changed, 3 insertions(+) diff --git a/app/layout.tsx b/app/layout.tsx index 085311c09..8134f4be1 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -3,6 +3,7 @@ import path from "node:path" import localFont from "next/font/local" import type { Metadata, Viewport } from "next" import WebsiteLanguage from "@/components/i18n/website-language" +import ArcadeFaqChatbot from "@/components/arcade/arcade-faq-chatbot" import CookieConsent from "@/components/privacy/cookie-consent" import { ThemeProvider } from "@/components/theme-provider" import ThemeToggle from "@/components/theme-toggle" @@ -33,6 +34,7 @@ import "./styles/shared-profile.css" import "./styles/profile-share.css" import "./styles/bonus-milestone.css" import "./styles/site-ui.css" +import "./styles/arcade-chatbot.css" const arcadeRequestDedupeBootstrap = readFileSync( path.join(process.cwd(), "scripts", "arcade-request-dedupe.js"), @@ -240,6 +242,7 @@ export default function RootLayout({ children }: Readonly<{ children: React.Reac {children} + Date: Sun, 4 Oct 2026 16:07:41 +0700 Subject: [PATCH 05/27] fix: use supported chatbot icons --- components/arcade/arcade-faq-chatbot.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/components/arcade/arcade-faq-chatbot.tsx b/components/arcade/arcade-faq-chatbot.tsx index 4177d20a0..f0884605e 100644 --- a/components/arcade/arcade-faq-chatbot.tsx +++ b/components/arcade/arcade-faq-chatbot.tsx @@ -2,8 +2,8 @@ import { ExternalLink, - MessageCircle, - Send, + CircleHelp, + Search, Sparkles, X, } from "lucide-react" @@ -475,7 +475,7 @@ export default function ArcadeFaqChatbot() { disabled={!input.trim()} aria-label={vietnameseUi ? "Gửi câu hỏi" : "Send question"} > - + @@ -490,7 +490,7 @@ export default function ArcadeFaqChatbot() { } onClick={() => setOpen((value) => !value)} > - + {vietnameseUi ? "Hỏi Arcade" : "Ask Arcade"} From 204fc9cfdc2dd4b53afa5f415833c0648c09a071 Mon Sep 17 00:00:00 2001 From: David Nguyen Date: Sun, 4 Oct 2026 17:27:03 +0700 Subject: [PATCH 06/27] feat: add chatbot conversation menu --- components/arcade/arcade-faq-chatbot.tsx | 104 +++++++++++++++++++++-- 1 file changed, 97 insertions(+), 7 deletions(-) diff --git a/components/arcade/arcade-faq-chatbot.tsx b/components/arcade/arcade-faq-chatbot.tsx index f0884605e..fe9d1e639 100644 --- a/components/arcade/arcade-faq-chatbot.tsx +++ b/components/arcade/arcade-faq-chatbot.tsx @@ -5,6 +5,10 @@ import { CircleHelp, Search, Sparkles, + MoreVertical, + RotateCcw, + ListRestart, + Info, X, } from "lucide-react" import type { FormEvent } from "react" @@ -270,6 +274,8 @@ function getLocale(): string { export default function ArcadeFaqChatbot() { const [open, setOpen] = useState(false) const [input, setInput] = useState("") + const [menuOpen, setMenuOpen] = useState(false) + const [showAbout, setShowAbout] = useState(false) const [locale, setLocale] = useState("en") const [hiddenForWidget, setHiddenForWidget] = useState(false) const [messages, setMessages] = useState([]) @@ -350,6 +356,22 @@ export default function ArcadeFaqChatbot() { setInput("") } + function clearChat() { + setMessages([]) + setInput("") + setMenuOpen(false) + setShowAbout(false) + messageId.current = 0 + window.setTimeout(() => inputRef.current?.focus(), 0) + } + + function showSuggestions() { + setMessages([]) + setMenuOpen(false) + setShowAbout(false) + window.setTimeout(() => inputRef.current?.focus(), 0) + } + function submit(event: FormEvent) { event.preventDefault() ask(input) @@ -378,16 +400,82 @@ export default function ArcadeFaqChatbot() { : "Quick FAQ · 2026 guidance"} - +
+ + + {menuOpen ? ( +
+ + + +
+ ) : null} +
+ {showAbout ? ( +
+
+ + {vietnameseUi ? "Về Arcade Guide" : "About Arcade Guide"} +
+

+ {vietnameseUi + ? "Đây là FAQ assistant chạy trực tiếp trên website, không gửi câu hỏi tới AI API. Hội thoại chỉ tồn tại trong phiên trang hiện tại và có thể xóa bằng New chat." + : "This FAQ assistant runs directly in the website and does not send questions to an AI API. Conversation history only lives in the current page session and can be cleared with New chat."} +

+ + {vietnameseUi ? "Rule & tier Arcade 2026" : "2026 Arcade rules & tiers"} + + + {vietnameseUi ? "Thông tin Prize Counter 2026" : "2026 Prize Counter guidance"} + + +
+ ) : null} + {!showAbout ? ( + <>
) : null} + + ) : null}
From c9246fb721ebb5445b528ee37c88febb92faaa77 Mon Sep 17 00:00:00 2001 From: David Nguyen Date: Sun, 4 Oct 2026 17:27:05 +0700 Subject: [PATCH 07/27] style: add chatbot menu and about panel --- app/styles/arcade-chatbot.css | 129 ++++++++++++++++++++++++++++++++-- 1 file changed, 125 insertions(+), 4 deletions(-) diff --git a/app/styles/arcade-chatbot.css b/app/styles/arcade-chatbot.css index a0f0ece84..c9c232487 100644 --- a/app/styles/arcade-chatbot.css +++ b/app/styles/arcade-chatbot.css @@ -53,7 +53,7 @@ .arcade-chatbot-header { display: grid; - grid-template-columns: 40px minmax(0, 1fr) 36px; + grid-template-columns: 40px minmax(0, 1fr) auto; align-items: center; gap: 10px; padding: 13px 14px; @@ -105,7 +105,13 @@ font-size: .66rem; } -.arcade-chatbot-header > button { +.arcade-chatbot-header-actions { + position: relative; + display: flex; + gap: 2px; +} + +.arcade-chatbot-header-actions > button { display: grid; place-items: center; width: 34px; @@ -118,13 +124,13 @@ cursor: pointer; } -.arcade-chatbot-header > button:hover { +.arcade-chatbot-header-actions > button:hover { border-color: var(--ui-border); background: var(--ui-soft); color: var(--ui-text); } -.arcade-chatbot-header > button svg { +.arcade-chatbot-header-actions > button svg { width: 16px; height: 16px; } @@ -349,3 +355,118 @@ scroll-behavior: auto; } } + + +.arcade-chatbot-menu { + position: absolute; + top: 40px; + right: 0; + z-index: 3; + width: 238px; + overflow: hidden; + padding: 5px; + border: 1px solid var(--ui-border); + border-radius: 12px; + background: var(--ui-surface); + box-shadow: 0 16px 40px hsl(224 50% 3% / .28); +} + +.arcade-chatbot-menu > button { + display: grid; + grid-template-columns: 28px minmax(0, 1fr); + gap: 8px; + width: 100%; + padding: 9px 8px; + border: 0; + border-radius: 8px; + background: transparent; + color: var(--ui-text); + text-align: left; + cursor: pointer; +} + +.arcade-chatbot-menu > button:hover { + background: var(--ui-soft); +} + +.arcade-chatbot-menu svg { + width: 15px; + height: 15px; + margin: 2px auto 0; + color: var(--ui-muted); +} + +.arcade-chatbot-menu strong, +.arcade-chatbot-menu small { + display: block; +} + +.arcade-chatbot-menu strong { + font-size: .68rem; +} + +.arcade-chatbot-menu small { + margin-top: 2px; + color: var(--ui-muted); + font-size: .58rem; + line-height: 1.35; +} + +.arcade-chatbot-about { + display: grid; + gap: 10px; + padding: 12px; + border: 1px solid var(--ui-border); + border-radius: 12px; + background: var(--ui-soft); +} + +.arcade-chatbot-about > div { + display: flex; + align-items: center; + gap: 7px; +} + +.arcade-chatbot-about > div svg { + width: 15px; + height: 15px; + color: var(--ui-accent); +} + +.arcade-chatbot-about strong { + font-size: .76rem; +} + +.arcade-chatbot-about p { + margin: 0; + color: var(--ui-muted); + font-size: .68rem; + line-height: 1.55; +} + +.arcade-chatbot-about a { + display: inline-flex; + align-items: center; + gap: 5px; + color: var(--ui-accent); + font-size: .63rem; + font-weight: 700; + text-decoration: none; +} + +.arcade-chatbot-about a svg { + width: 10px; + height: 10px; +} + +.arcade-chatbot-about > button { + justify-self: start; + padding: 7px 10px; + border: 1px solid var(--ui-border); + border-radius: 8px; + background: var(--ui-surface); + color: var(--ui-text); + font-size: .64rem; + font-weight: 700; + cursor: pointer; +} From 4baa2057de1a73191aa9f8c38fd999543ca9510b Mon Sep 17 00:00:00 2001 From: David Nguyen Date: Sun, 4 Oct 2026 17:27:08 +0700 Subject: [PATCH 08/27] test: cover chatbot conversation controls --- tests/arcade-chatbot.test.mjs | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/tests/arcade-chatbot.test.mjs b/tests/arcade-chatbot.test.mjs index 938df3cc0..6675ca169 100644 --- a/tests/arcade-chatbot.test.mjs +++ b/tests/arcade-chatbot.test.mjs @@ -36,3 +36,15 @@ test("Arcade FAQ chatbot is responsive and avoids the mobile bottom navigation", assert.match(chatbotCss, /bottom: 84px/) assert.match(chatbotCss, /100dvh/) }) + + +test("Arcade FAQ chatbot exposes conversation controls", () => { + assert.match(chatbot, /Chat menu/) + assert.match(chatbot, /New chat/) + assert.match(chatbot, /Suggested questions/) + assert.match(chatbot, /About & sources/) + assert.match(chatbot, /setMessages\(\[\]\)/) + assert.match(chatbot, /Conversation history only lives in the current page session/) + assert.match(chatbotCss, /arcade-chatbot-menu/) + assert.match(chatbotCss, /arcade-chatbot-about/) +}) From 2d4719356ef1c60195a6cbc9f761748261d6f03d Mon Sep 17 00:00:00 2001 From: David Nguyen Date: Mon, 5 Oct 2026 08:19:28 +0700 Subject: [PATCH 09/27] fix: use supported chatbot menu icon --- components/arcade/arcade-faq-chatbot.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/components/arcade/arcade-faq-chatbot.tsx b/components/arcade/arcade-faq-chatbot.tsx index fe9d1e639..cd56e1976 100644 --- a/components/arcade/arcade-faq-chatbot.tsx +++ b/components/arcade/arcade-faq-chatbot.tsx @@ -7,7 +7,6 @@ import { Sparkles, MoreVertical, RotateCcw, - ListRestart, Info, X, } from "lucide-react" @@ -426,7 +425,7 @@ export default function ArcadeFaqChatbot() {
From 856fa06b5bfcebff90add71349271d8b910b5da9 Mon Sep 17 00:00:00 2001 From: David Nguyen Date: Mon, 5 Oct 2026 11:53:15 +0700 Subject: [PATCH 11/27] sync: inherit app/styles/back-to-top.css from #89 --- app/styles/back-to-top.css | 55 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 55 insertions(+) create mode 100644 app/styles/back-to-top.css diff --git a/app/styles/back-to-top.css b/app/styles/back-to-top.css new file mode 100644 index 000000000..bbb87c6b2 --- /dev/null +++ b/app/styles/back-to-top.css @@ -0,0 +1,55 @@ +.back-to-top { + position: fixed; + right: 22px; + bottom: 22px; + z-index: 92; + display: grid; + place-items: center; + width: 42px; + height: 42px; + padding: 0; + border: 1px solid rgba(139, 92, 246, 0.45); + border-radius: 50%; + background: rgba(91, 33, 182, 0.94); + color: #fff; + box-shadow: 0 10px 30px rgba(0, 0, 0, 0.28); + cursor: pointer; + backdrop-filter: blur(12px); + transition: transform 160ms ease, background 160ms ease, box-shadow 160ms ease; +} + +.back-to-top:hover { + transform: translateY(-2px); + background: rgba(109, 40, 217, 0.98); + box-shadow: 0 14px 34px rgba(0, 0, 0, 0.34); +} + +.back-to-top:focus-visible { + outline: 2px solid #67e8f9; + outline-offset: 3px; +} + +.back-to-top svg { + width: 18px; + height: 18px; +} + +html.light .back-to-top { + border-color: rgba(109, 40, 217, 0.3); + background: rgba(109, 40, 217, 0.92); +} + +@media (max-width: 640px) { + .back-to-top { + right: 14px; + bottom: calc(82px + env(safe-area-inset-bottom)); + width: 40px; + height: 40px; + } +} + +@media (prefers-reduced-motion: reduce) { + .back-to-top { + transition: none; + } +} From b57e394074547d88b27242648a92726ed6fadb4d Mon Sep 17 00:00:00 2001 From: David Nguyen Date: Mon, 5 Oct 2026 11:53:19 +0700 Subject: [PATCH 12/27] sync: inherit app/styles/home-refactor.css from #89 --- app/styles/home-refactor.css | 694 +++++++++++++++++++++++++---------- 1 file changed, 508 insertions(+), 186 deletions(-) diff --git a/app/styles/home-refactor.css b/app/styles/home-refactor.css index 62039e653..6b514795b 100644 --- a/app/styles/home-refactor.css +++ b/app/styles/home-refactor.css @@ -1,51 +1,180 @@ /* Home dashboard visual refactor (v2). - * Scoped to .arcade-dashboard-v2 so widgets and internal pages retain their own themes. + * The established profile dashboard keeps the legacy visual system. + * V2 surface/layout overrides are intentionally scoped to guest mode; preview controls + * remain shared so PR-only Guest/Profile switching stays usable. */ -.arcade-dashboard-v2 { - --v2-shell: var(--ui-width); - --v2-gutter: var(--ui-gutter); - --v2-radius-lg: var(--ui-radius-lg); - --v2-radius-md: var(--ui-radius); - --v2-radius-sm: var(--ui-radius-sm); - --v2-surface: var(--ui-surface); - --v2-surface-strong: var(--ui-surface); - --v2-surface-soft: var(--ui-soft); - --v2-border: var(--ui-border); - --v2-border-strong: var(--ui-accent-border); - --v2-shadow: var(--ui-shadow); - --v2-shadow-soft: var(--ui-shadow); -} - -.arcade-dashboard-v2 .program-countdown-host, -.arcade-dashboard-v2 .extension-strip, -.arcade-dashboard-v2 .dashboard-shell, -.arcade-dashboard-v2 .dashboard-empty-state, -.arcade-dashboard-v2 .arcade-footer { +.arcade-dashboard-v2[data-dashboard-view="guest"] { + --v2-shell: 1200px; + --v2-gutter: 24px; + --v2-radius-lg: 20px; + --v2-radius-md: 16px; + --v2-radius-sm: 12px; + --v2-surface: rgba(9, 17, 36, 0.78); + --v2-surface-strong: rgba(10, 19, 40, 0.94); + --v2-surface-soft: rgba(15, 27, 52, 0.62); + --v2-border: rgba(148, 163, 184, 0.14); + --v2-border-strong: rgba(139, 92, 246, 0.28); + --v2-shadow: 0 12px 32px rgba(0, 0, 0, 0.16); + --v2-shadow-soft: 0 4px 16px rgba(0, 0, 0, 0.10); + background: + radial-gradient(circle at 82% 5%, rgba(99, 102, 241, 0.16), transparent 31rem), + radial-gradient(circle at 12% 24%, rgba(14, 165, 233, 0.11), transparent 29rem), + linear-gradient(180deg, #050918 0%, #071126 48%, #050918 100%); +} + +html.light .arcade-dashboard-v2[data-dashboard-view="guest"] { + --v2-surface: rgba(255, 255, 255, 0.82); + --v2-surface-strong: rgba(255, 255, 255, 0.96); + --v2-surface-soft: rgba(248, 250, 252, 0.82); + --v2-border: rgba(100, 116, 139, 0.15); + --v2-border-strong: rgba(79, 70, 229, 0.20); + --v2-shadow: 0 22px 58px rgba(51, 65, 85, 0.11); + --v2-shadow-soft: 0 10px 30px rgba(51, 65, 85, 0.08); + background: + radial-gradient(circle at 82% 5%, rgba(99, 102, 241, 0.11), transparent 31rem), + radial-gradient(circle at 12% 24%, rgba(14, 165, 233, 0.08), transparent 29rem), + linear-gradient(180deg, #f8faff 0%, #eef3ff 48%, #f8faff 100%); +} + +.arcade-dashboard-v2[data-dashboard-view="guest"] .arcade-header, +.arcade-dashboard-v2[data-dashboard-view="guest"] .arcade-hero, +.arcade-dashboard-v2[data-dashboard-view="guest"] .program-countdown-host, +.arcade-dashboard-v2[data-dashboard-view="guest"] .extension-strip, +.arcade-dashboard-v2[data-dashboard-view="guest"] .dashboard-shell, +.arcade-dashboard-v2[data-dashboard-view="guest"] .dashboard-empty-state, +.arcade-dashboard-v2[data-dashboard-view="guest"] .arcade-footer { width: min(var(--v2-shell), calc(100% - (var(--v2-gutter) * 2))); } -.arcade-dashboard-v2 .arcade-nav { +/* Header: compact floating workspace bar instead of a full-width divider. */ +.arcade-dashboard-v2[data-dashboard-view="guest"] .arcade-header { + top: 10px; + min-height: 64px; + margin-top: 10px; + padding: 0 12px 0 14px; + border: 1px solid var(--v2-border); + border-radius: var(--v2-radius-md); + background: var(--v2-surface-strong); + box-shadow: var(--v2-shadow-soft); + backdrop-filter: blur(20px) saturate(130%); +} + +.arcade-dashboard-v2[data-dashboard-view="guest"] .arcade-brand-mark { + width: 36px; + height: 36px; + border-radius: 11px; +} + +.arcade-dashboard-v2[data-dashboard-view="guest"] .arcade-brand-copy { + font-size: 0.68rem; +} + +.arcade-dashboard-v2[data-dashboard-view="guest"] .arcade-brand em { + padding: 4px 6px; + border-radius: 6px; +} + +.arcade-dashboard-v2[data-dashboard-view="guest"] .arcade-nav { gap: 2px; } -.arcade-dashboard-v2 .arcade-nav a { +.arcade-dashboard-v2[data-dashboard-view="guest"] .arcade-nav a { padding: 20px 11px 18px; border-radius: 9px; font-size: 0.8rem; } -.arcade-dashboard-v2 .arcade-nav a::after { +.arcade-dashboard-v2[data-dashboard-view="guest"] .arcade-nav a::after { left: 11px; right: 11px; } -.arcade-dashboard-v2 .header-store-link { +.arcade-dashboard-v2[data-dashboard-view="guest"] .arcade-header-actions { + gap: 7px; +} + +.arcade-dashboard-v2[data-dashboard-view="guest"] .header-store-link { min-height: 38px; border-radius: 10px; } -.arcade-dashboard-v2 .dashboard-view-switch { +/* Hero: clearer hierarchy and a stronger analyzer focal point. */ +.arcade-dashboard-v2[data-dashboard-view="guest"] .arcade-hero { + grid-template-columns: minmax(0, 1.08fr) minmax(360px, 0.92fr); + gap: clamp(32px, 5vw, 72px); + align-items: center; + padding: clamp(54px, 7vw, 86px) 0 38px; +} + +.arcade-dashboard-v2[data-dashboard-view="guest"] .hero-heading { + max-width: 690px; +} + +.arcade-dashboard-v2[data-dashboard-view="guest"] .pixel-kicker { + width: fit-content; + padding: 7px 10px; + border: 1px solid rgba(34, 211, 238, 0.17); + border-radius: 999px; + background: rgba(8, 145, 178, 0.06); +} + +html.light .arcade-dashboard-v2[data-dashboard-view="guest"] .pixel-kicker { + border-color: rgba(2, 132, 199, 0.15); + background: rgba(224, 242, 254, 0.7); +} + +.arcade-dashboard-v2[data-dashboard-view="guest"] .hero-heading h1 { + margin: 22px 0 18px; + max-width: 760px; + font-size: clamp(2.25rem, 4.4vw, 4.35rem); + line-height: 1.13; + letter-spacing: -0.055em; + text-wrap: balance; +} + +.arcade-dashboard-v2[data-dashboard-view="guest"] .hero-description { + max-width: 610px; + font-size: 1rem; + line-height: 1.72; +} + +.arcade-dashboard-v2[data-dashboard-view="guest"] .trust-pills { + gap: 8px; + margin-top: 22px; +} + +.arcade-dashboard-v2[data-dashboard-view="guest"] .trust-pills span { + min-height: 34px; + padding: 7px 11px; + border-radius: 999px; +} + +.arcade-dashboard-v2[data-dashboard-view="guest"] .profile-analyzer-card { + position: relative; + padding: 28px; + overflow: hidden; + border-color: var(--v2-border-strong); + border-radius: var(--v2-radius-lg); + background: var(--v2-surface-strong); + box-shadow: var(--v2-shadow); +} + +.arcade-dashboard-v2[data-dashboard-view="guest"] .profile-analyzer-card::before { + content: ""; + position: absolute; + inset: 0 0 auto; + height: 2px; + background: linear-gradient(90deg, transparent, var(--arcade-purple), var(--arcade-cyan), transparent); + opacity: 0.85; +} + +.arcade-dashboard-v2[data-dashboard-view="guest"] .analyzer-title { + margin-bottom: 18px; + font-size: 0.72rem; +} + +.arcade-dashboard-v2[data-dashboard-view="guest"] .dashboard-view-switch { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 6px; @@ -56,7 +185,7 @@ background: var(--v2-surface-soft); } -.arcade-dashboard-v2 .dashboard-view-switch button { +.arcade-dashboard-v2[data-dashboard-view="guest"] .dashboard-view-switch button { display: inline-flex; align-items: center; justify-content: center; @@ -74,37 +203,59 @@ transition: background 160ms ease, border-color 160ms ease, color 160ms ease, transform 160ms ease; } -.arcade-dashboard-v2 .dashboard-view-switch button svg { +.arcade-dashboard-v2[data-dashboard-view="guest"] .dashboard-view-switch button svg { width: 16px; height: 16px; } -.arcade-dashboard-v2 .dashboard-view-switch button:hover { +.arcade-dashboard-v2[data-dashboard-view="guest"] .dashboard-view-switch button:hover { color: var(--arcade-text); } -.arcade-dashboard-v2 .dashboard-view-switch button.is-active { +.arcade-dashboard-v2[data-dashboard-view="guest"] .dashboard-view-switch button.is-active { border-color: var(--v2-border-strong); background: var(--v2-surface-strong); color: var(--arcade-text); box-shadow: 0 6px 18px rgba(0, 0, 0, 0.12); } -.arcade-dashboard-v2 .dashboard-view-switch button:focus-visible { +.arcade-dashboard-v2[data-dashboard-view="guest"] .dashboard-view-switch button:focus-visible { outline: 2px solid var(--arcade-cyan); outline-offset: 2px; } +.arcade-dashboard-v2[data-dashboard-view="guest"] .profile-analyzer-card form { + grid-template-columns: minmax(0, 1fr); + gap: 10px; +} + +.arcade-dashboard-v2[data-dashboard-view="guest"] .profile-input { + min-height: 58px; + padding: 0 15px; + border-radius: var(--v2-radius-sm); + background: var(--v2-surface-soft); +} + +.arcade-dashboard-v2[data-dashboard-view="guest"] .analyze-button { + min-height: 50px; + border-radius: var(--v2-radius-sm); + box-shadow: 0 12px 28px rgba(124, 58, 237, 0.22); +} + +.arcade-dashboard-v2[data-dashboard-view="guest"] .analyzer-help-row { + margin-top: 14px; +} + /* Countdown uses the same desktop shell and card language as the rest of the workspace. */ -.arcade-dashboard-v2 .program-countdown-host { +.arcade-dashboard-v2[data-dashboard-view="guest"] .program-countdown-host { margin-bottom: 22px; } -.arcade-dashboard-v2 .program-countdown-grid { +.arcade-dashboard-v2[data-dashboard-view="guest"] .program-countdown-grid { gap: 16px; } -.arcade-dashboard-v2 .program-countdown-card { +.arcade-dashboard-v2[data-dashboard-view="guest"] .program-countdown-card { border-color: var(--v2-border); border-radius: var(--v2-radius-md); background: @@ -113,19 +264,19 @@ box-shadow: var(--v2-shadow-soft); } -html.light .arcade-dashboard-v2 .program-countdown-card { +html.light .arcade-dashboard-v2[data-dashboard-view="guest"] .program-countdown-card { background: radial-gradient(circle at 10% 10%, var(--countdown-accent-soft), transparent 52%), var(--v2-surface); } -.arcade-dashboard-v2 .program-countdown-part { +.arcade-dashboard-v2[data-dashboard-view="guest"] .program-countdown-part { border-color: var(--v2-border); background: var(--v2-surface-soft); } /* Secondary browser CTA becomes part of the same visual system. */ -.arcade-dashboard-v2 .extension-strip { +.arcade-dashboard-v2[data-dashboard-view="guest"] .extension-strip { min-height: 70px; margin-top: 4px; margin-bottom: 22px; @@ -136,71 +287,71 @@ html.light .arcade-dashboard-v2 .program-countdown-card { box-shadow: var(--v2-shadow-soft); } -.arcade-dashboard-v2 .extension-store-mark { +.arcade-dashboard-v2[data-dashboard-view="guest"] .extension-store-mark { width: 42px; height: 42px; border-radius: 12px; } -.arcade-dashboard-v2 .extension-store-actions a { +.arcade-dashboard-v2[data-dashboard-view="guest"] .extension-store-actions a { min-height: 42px; border-radius: 10px; } /* Results: consistent rhythm, less empty height and stronger grouping. */ -.arcade-dashboard-v2 .dashboard-summary-grid, -.arcade-dashboard-v2 .dashboard-content-grid, -.arcade-dashboard-v2 .dashboard-bottom-grid { +.arcade-dashboard-v2[data-dashboard-view="guest"] .dashboard-summary-grid, +.arcade-dashboard-v2[data-dashboard-view="guest"] .dashboard-content-grid, +.arcade-dashboard-v2[data-dashboard-view="guest"] .dashboard-bottom-grid { gap: 16px; margin-bottom: 16px; } -.arcade-dashboard-v2 .dashboard-summary-grid { +.arcade-dashboard-v2[data-dashboard-view="guest"] .dashboard-summary-grid { grid-template-columns: minmax(0, 1.05fr) minmax(0, 1.05fr) minmax(0, 0.9fr); } -.arcade-dashboard-v2 .dashboard-content-grid { +.arcade-dashboard-v2[data-dashboard-view="guest"] .dashboard-content-grid { grid-template-columns: minmax(0, 1fr) 360px; } -.arcade-dashboard-v2 .dashboard-panel, -.arcade-dashboard-v2 .dashboard-empty-state { +.arcade-dashboard-v2[data-dashboard-view="guest"] .dashboard-panel, +.arcade-dashboard-v2[data-dashboard-view="guest"] .dashboard-empty-state { border-color: var(--v2-border); border-radius: var(--v2-radius-md); background: var(--v2-surface); box-shadow: var(--v2-shadow-soft); } -.arcade-dashboard-v2 .dashboard-panel { +.arcade-dashboard-v2[data-dashboard-view="guest"] .dashboard-panel { padding: 22px; } -.arcade-dashboard-v2 .dashboard-summary-grid > .dashboard-panel { +.arcade-dashboard-v2[data-dashboard-view="guest"] .dashboard-summary-grid > .dashboard-panel { min-height: 0; } -.arcade-dashboard-v2 .panel-title { +.arcade-dashboard-v2[data-dashboard-view="guest"] .panel-title { font-size: 0.71rem; letter-spacing: 0.055em; } -.arcade-dashboard-v2 .profile-overview { +.arcade-dashboard-v2[data-dashboard-view="guest"] .profile-overview { min-height: 0; margin: 20px 0; } -.arcade-dashboard-v2 .profile-photo { +.arcade-dashboard-v2[data-dashboard-view="guest"] .profile-photo { width: 76px; height: 76px; } -.arcade-dashboard-v2 .profile-stat-grid { +.arcade-dashboard-v2[data-dashboard-view="guest"] .profile-stat-grid { gap: 8px; margin-top: auto; padding-top: 16px; } -.arcade-dashboard-v2 .profile-stat-grid > div { +.arcade-dashboard-v2[data-dashboard-view="guest"] .profile-stat-grid > div { min-height: 82px; padding: 10px 8px; border: 1px solid var(--v2-border); @@ -208,62 +359,62 @@ html.light .arcade-dashboard-v2 .program-countdown-card { background: var(--v2-surface-soft); } -.arcade-dashboard-v2 .profile-stat-grid > div + div { +.arcade-dashboard-v2[data-dashboard-view="guest"] .profile-stat-grid > div + div { border-left: 1px solid var(--v2-border); } -.arcade-dashboard-v2 .points-total { +.arcade-dashboard-v2[data-dashboard-view="guest"] .points-total { margin: 20px 0 16px; } -.arcade-dashboard-v2 .points-total strong { +.arcade-dashboard-v2[data-dashboard-view="guest"] .points-total strong { font-size: 2.45rem; letter-spacing: -0.04em; } -.arcade-dashboard-v2 .point-composition-bar { +.arcade-dashboard-v2[data-dashboard-view="guest"] .point-composition-bar { height: 14px; margin-bottom: 14px; border-radius: 999px; } -.arcade-dashboard-v2 .point-breakdown-row { +.arcade-dashboard-v2[data-dashboard-view="guest"] .point-breakdown-row { min-height: 44px; padding: 9px 0; } -.arcade-dashboard-v2 .tier-status-main { +.arcade-dashboard-v2[data-dashboard-view="guest"] .tier-status-main { margin: 20px 0 16px; } -.arcade-dashboard-v2 .tier-availability-grid > div { +.arcade-dashboard-v2[data-dashboard-view="guest"] .tier-availability-grid > div { min-height: 82px; border-color: var(--v2-border); border-radius: var(--v2-radius-sm); background: var(--v2-surface-soft); } -.arcade-dashboard-v2 .badge-heading-row { +.arcade-dashboard-v2[data-dashboard-view="guest"] .badge-heading-row { align-items: flex-start; gap: 14px; } -.arcade-dashboard-v2 .badge-filters { +.arcade-dashboard-v2[data-dashboard-view="guest"] .badge-filters { gap: 6px; } -.arcade-dashboard-v2 .badge-filters button { +.arcade-dashboard-v2[data-dashboard-view="guest"] .badge-filters button { min-height: 34px; padding-inline: 12px; border-radius: 999px; } -.arcade-dashboard-v2 .earned-badge-grid { +.arcade-dashboard-v2[data-dashboard-view="guest"] .earned-badge-grid { grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 10px; } -.arcade-dashboard-v2 .earned-badge { +.arcade-dashboard-v2[data-dashboard-view="guest"] .earned-badge { min-height: 0; padding: 12px; border-color: var(--v2-border); @@ -273,21 +424,21 @@ html.light .arcade-dashboard-v2 .program-countdown-card { transition: transform 160ms ease, border-color 160ms ease, box-shadow 160ms ease; } -.arcade-dashboard-v2 .earned-badge:hover { +.arcade-dashboard-v2[data-dashboard-view="guest"] .earned-badge:hover { transform: translateY(-2px); border-color: var(--v2-border-strong); box-shadow: var(--v2-shadow-soft); } -.arcade-dashboard-v2 .earned-badge-art { +.arcade-dashboard-v2[data-dashboard-view="guest"] .earned-badge-art { border-radius: 11px; } -.arcade-dashboard-v2 .tier-list { +.arcade-dashboard-v2[data-dashboard-view="guest"] .tier-list { gap: 7px; } -.arcade-dashboard-v2 .tier-list-row { +.arcade-dashboard-v2[data-dashboard-view="guest"] .tier-list-row { min-height: 64px; padding: 10px 11px; border: 1px solid var(--v2-border); @@ -295,20 +446,20 @@ html.light .arcade-dashboard-v2 .program-countdown-card { background: var(--v2-surface-soft); } -.arcade-dashboard-v2 .tier-list-row.is-current { +.arcade-dashboard-v2[data-dashboard-view="guest"] .tier-list-row.is-current { border-color: var(--v2-border-strong); box-shadow: inset 3px 0 0 var(--arcade-purple); } -.arcade-dashboard-v2 .dashboard-bottom-grid { +.arcade-dashboard-v2[data-dashboard-view="guest"] .dashboard-bottom-grid { grid-template-columns: repeat(3, minmax(0, 1fr)); } -.arcade-dashboard-v2 .compact-panel { +.arcade-dashboard-v2[data-dashboard-view="guest"] .compact-panel { min-height: 0; } -.arcade-dashboard-v2 .goal-progress { +.arcade-dashboard-v2[data-dashboard-view="guest"] .goal-progress { height: 9px; border-radius: 999px; } @@ -367,7 +518,7 @@ html.light .arcade-dashboard-v2 .preview-mode-toolbar { } /* Guest dashboard: compact, visual and intentionally separate from profile data. */ -.arcade-dashboard-v2 .dashboard-empty-state.guest-dashboard { +.arcade-dashboard-v2[data-dashboard-view="guest"] .dashboard-empty-state.guest-dashboard { display: grid; grid-template-columns: 1fr; gap: 20px; @@ -378,7 +529,7 @@ html.light .arcade-dashboard-v2 .preview-mode-toolbar { box-shadow: none; } -.arcade-dashboard-v2 .guest-dashboard-hero { +.arcade-dashboard-v2[data-dashboard-view="guest"] .guest-dashboard-hero { position: relative; isolation: isolate; display: grid; @@ -399,7 +550,7 @@ html.light .arcade-dashboard-v2 .preview-mode-toolbar { 0 18px 44px rgba(0, 0, 0, 0.16); } -.arcade-dashboard-v2 .guest-dashboard-hero::before { +.arcade-dashboard-v2[data-dashboard-view="guest"] .guest-dashboard-hero::before { content: ""; position: absolute; inset: 0; @@ -412,7 +563,7 @@ html.light .arcade-dashboard-v2 .preview-mode-toolbar { mask-image: linear-gradient(90deg, transparent, black 18%, black 82%, transparent); } -.arcade-dashboard-v2 .guest-dashboard-orb { +.arcade-dashboard-v2[data-dashboard-view="guest"] .guest-dashboard-orb { position: relative; display: grid; place-items: center; @@ -429,25 +580,25 @@ html.light .arcade-dashboard-v2 .preview-mode-toolbar { 0 0 34px rgba(255, 193, 7, 0.16); } -.arcade-dashboard-v2 .guest-dashboard-orb svg { +.arcade-dashboard-v2[data-dashboard-view="guest"] .guest-dashboard-orb svg { width: 38px; height: 38px; } -.arcade-dashboard-v2 .guest-dashboard-copy { +.arcade-dashboard-v2[data-dashboard-view="guest"] .guest-dashboard-copy { min-width: 0; display: grid; gap: 10px; } -.arcade-dashboard-v2 .guest-dashboard-copy strong { +.arcade-dashboard-v2[data-dashboard-view="guest"] .guest-dashboard-copy strong { color: var(--arcade-text); font-size: clamp(1.35rem, 2.3vw, 2rem); line-height: 1.15; letter-spacing: -0.035em; } -.arcade-dashboard-v2 .guest-dashboard-copy p { +.arcade-dashboard-v2[data-dashboard-view="guest"] .guest-dashboard-copy p { max-width: 620px; margin: 0; color: var(--arcade-muted); @@ -455,7 +606,7 @@ html.light .arcade-dashboard-v2 .preview-mode-toolbar { line-height: 1.65; } -.arcade-dashboard-v2 .guest-dashboard-decoration { +.arcade-dashboard-v2[data-dashboard-view="guest"] .guest-dashboard-decoration { align-self: stretch; display: grid; place-items: center; @@ -464,13 +615,13 @@ html.light .arcade-dashboard-v2 .preview-mode-toolbar { transform: rotate(-12deg) translateX(18px); } -.arcade-dashboard-v2 .guest-dashboard-decoration svg { +.arcade-dashboard-v2[data-dashboard-view="guest"] .guest-dashboard-decoration svg { width: 110px; height: 110px; stroke-width: 1.35; } -html.light .arcade-dashboard-v2 .guest-dashboard-hero { +html.light .arcade-dashboard-v2[data-dashboard-view="guest"] .guest-dashboard-hero { border-color: rgba(99, 102, 241, 0.24); background: radial-gradient(circle at 18% 50%, rgba(251, 191, 36, 0.13), transparent 25%), @@ -478,20 +629,20 @@ html.light .arcade-dashboard-v2 .guest-dashboard-hero { linear-gradient(135deg, rgba(255, 255, 255, 0.98), rgba(246, 248, 255, 0.96)); } -.arcade-dashboard-v2 .guest-tier-heading { +.arcade-dashboard-v2[data-dashboard-view="guest"] .guest-tier-heading { display: flex; align-items: center; justify-content: space-between; margin-top: 2px; } -.arcade-dashboard-v2 .guest-tier-heading > div { +.arcade-dashboard-v2[data-dashboard-view="guest"] .guest-tier-heading > div { display: flex; align-items: center; gap: 11px; } -.arcade-dashboard-v2 .guest-tier-heading-icon { +.arcade-dashboard-v2[data-dashboard-view="guest"] .guest-tier-heading-icon { display: grid; place-items: center; width: 36px; @@ -502,17 +653,17 @@ html.light .arcade-dashboard-v2 .guest-dashboard-hero { color: #c054ff; } -.arcade-dashboard-v2 .guest-tier-heading-icon svg { +.arcade-dashboard-v2[data-dashboard-view="guest"] .guest-tier-heading-icon svg { width: 18px; height: 18px; } -.arcade-dashboard-v2 .guest-tier-heading > div > div { +.arcade-dashboard-v2[data-dashboard-view="guest"] .guest-tier-heading > div > div { display: grid; gap: 2px; } -.arcade-dashboard-v2 .guest-tier-heading strong { +.arcade-dashboard-v2[data-dashboard-view="guest"] .guest-tier-heading strong { color: var(--arcade-text); font-size: 0.88rem; font-weight: 850; @@ -520,18 +671,18 @@ html.light .arcade-dashboard-v2 .guest-dashboard-hero { text-transform: uppercase; } -.arcade-dashboard-v2 .guest-tier-heading span { +.arcade-dashboard-v2[data-dashboard-view="guest"] .guest-tier-heading span { color: var(--arcade-muted); font-size: 0.68rem; } -.arcade-dashboard-v2 .empty-tier-grid { +.arcade-dashboard-v2[data-dashboard-view="guest"] .empty-tier-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 10px; } -.arcade-dashboard-v2 .guest-tier-card { +.arcade-dashboard-v2[data-dashboard-view="guest"] .guest-tier-card { --tier-accent: #8b5cf6; --tier-soft: rgba(139, 92, 246, 0.12); --tier-border: rgba(139, 92, 246, 0.30); @@ -549,45 +700,45 @@ html.light .arcade-dashboard-v2 .guest-dashboard-hero { box-shadow: none; } -.arcade-dashboard-v2 .shared-tier-panel .empty-tier-grid { +.arcade-dashboard-v2[data-dashboard-view="guest"] .shared-tier-panel .empty-tier-grid { grid-template-columns: minmax(0, 1fr); } -.arcade-dashboard-v2 .guest-tier-card.is-current { +.arcade-dashboard-v2[data-dashboard-view="guest"] .guest-tier-card.is-current { border-color: var(--tier-accent); background: linear-gradient(90deg, var(--tier-soft), transparent), var(--v2-surface-soft); box-shadow: inset 3px 0 0 var(--tier-accent); } -.arcade-dashboard-v2 .guest-tier-card.is-current .guest-tier-icon { +.arcade-dashboard-v2[data-dashboard-view="guest"] .guest-tier-card.is-current .guest-tier-icon { border-color: var(--tier-accent); } -.arcade-dashboard-v2 .guest-tier-card.tier-120 { +.arcade-dashboard-v2[data-dashboard-view="guest"] .guest-tier-card.tier-120 { --tier-accent: #ffd43b; --tier-soft: rgba(255, 212, 59, 0.09); --tier-border: rgba(255, 212, 59, 0.28); } -.arcade-dashboard-v2 .guest-tier-card.tier-95 { +.arcade-dashboard-v2[data-dashboard-view="guest"] .guest-tier-card.tier-95 { --tier-accent: #c084fc; --tier-soft: rgba(192, 132, 252, 0.10); --tier-border: rgba(192, 132, 252, 0.30); } -.arcade-dashboard-v2 .guest-tier-card.tier-75 { +.arcade-dashboard-v2[data-dashboard-view="guest"] .guest-tier-card.tier-75 { --tier-accent: #3b82f6; --tier-soft: rgba(59, 130, 246, 0.10); --tier-border: rgba(59, 130, 246, 0.30); } -.arcade-dashboard-v2 .guest-tier-card.tier-50 { +.arcade-dashboard-v2[data-dashboard-view="guest"] .guest-tier-card.tier-50 { --tier-accent: #34d399; --tier-soft: rgba(52, 211, 153, 0.10); --tier-border: rgba(52, 211, 153, 0.28); } -.arcade-dashboard-v2 .guest-tier-icon { +.arcade-dashboard-v2[data-dashboard-view="guest"] .guest-tier-icon { display: grid; place-items: center; width: 52px; @@ -599,24 +750,24 @@ html.light .arcade-dashboard-v2 .guest-dashboard-hero { box-shadow: 0 0 24px var(--tier-soft); } -.arcade-dashboard-v2 .guest-tier-icon svg { +.arcade-dashboard-v2[data-dashboard-view="guest"] .guest-tier-icon svg { width: 25px; height: 25px; } -.arcade-dashboard-v2 .guest-tier-main { +.arcade-dashboard-v2[data-dashboard-view="guest"] .guest-tier-main { min-width: 0; display: grid; gap: 11px; } -.arcade-dashboard-v2 .guest-tier-copy { +.arcade-dashboard-v2[data-dashboard-view="guest"] .guest-tier-copy { min-width: 0; display: grid; gap: 3px; } -.arcade-dashboard-v2 .guest-tier-copy strong { +.arcade-dashboard-v2[data-dashboard-view="guest"] .guest-tier-copy strong { overflow: hidden; color: var(--arcade-text); font-size: 0.86rem; @@ -627,19 +778,19 @@ html.light .arcade-dashboard-v2 .guest-dashboard-hero { white-space: nowrap; } -.arcade-dashboard-v2 .guest-tier-copy span { +.arcade-dashboard-v2[data-dashboard-view="guest"] .guest-tier-copy span { color: var(--arcade-muted); font-size: 0.7rem; } -.arcade-dashboard-v2 .guest-tier-progress { +.arcade-dashboard-v2[data-dashboard-view="guest"] .guest-tier-progress { height: 7px; overflow: hidden; border-radius: 999px; background: rgba(100, 116, 139, 0.17); } -.arcade-dashboard-v2 .guest-tier-progress > span { +.arcade-dashboard-v2[data-dashboard-view="guest"] .guest-tier-progress > span { display: block; height: 100%; border-radius: inherit; @@ -647,7 +798,7 @@ html.light .arcade-dashboard-v2 .guest-dashboard-hero { box-shadow: 0 0 14px var(--tier-soft); } -.arcade-dashboard-v2 .guest-tier-availability { +.arcade-dashboard-v2[data-dashboard-view="guest"] .guest-tier-availability { min-width: 112px; display: grid; justify-items: end; @@ -657,30 +808,98 @@ html.light .arcade-dashboard-v2 .guest-dashboard-hero { text-align: right; } -.arcade-dashboard-v2 .guest-tier-availability > strong { +.arcade-dashboard-v2[data-dashboard-view="guest"] .guest-tier-availability > strong { color: var(--arcade-text); font-size: 0.9rem; font-weight: 850; font-variant-numeric: tabular-nums; } -.arcade-dashboard-v2 .guest-tier-availability > strong span { +.arcade-dashboard-v2[data-dashboard-view="guest"] .guest-tier-availability > strong span { color: var(--arcade-muted); font-weight: 600; } -.arcade-dashboard-v2 .guest-tier-availability small { +.arcade-dashboard-v2[data-dashboard-view="guest"] .guest-tier-availability small { color: var(--arcade-muted); font-size: 0.62rem; } -.arcade-dashboard-v2 .guest-tier-availability .tier-slot-delta { +.arcade-dashboard-v2[data-dashboard-view="guest"] .guest-tier-availability .tier-slot-delta { margin-top: 1px; font-size: 0.58rem; } + +/* Shared tier renderer is used by both Guest and Profile modes. + * Keep these component styles global to v2 without reapplying the full profile redesign. */ +.arcade-dashboard-v2 .shared-tier-panel { + container-type: inline-size; +} +.arcade-dashboard-v2 .shared-tier-panel .empty-tier-grid { + display: grid; + grid-template-columns: minmax(0, 1fr); + gap: 7px; +} +.arcade-dashboard-v2 .shared-tier-panel .guest-tier-card { + --tier-accent: #8b5cf6; + --tier-soft: rgba(139, 92, 246, 0.12); + --tier-border: rgba(139, 92, 246, 0.30); + display: grid; + grid-template-columns: 38px minmax(0, 1fr) auto; + align-items: center; + gap: 9px; + min-height: 64px; + padding: 9px 10px; + overflow: hidden; + border: 1px solid var(--arcade-border-soft); + border-radius: 10px; + background: rgba(13, 25, 49, 0.72); +} +.arcade-dashboard-v2 .shared-tier-panel .guest-tier-card.tier-120 { --tier-accent:#ffd43b; --tier-soft:rgba(255,212,59,.09); --tier-border:rgba(255,212,59,.28); } +.arcade-dashboard-v2 .shared-tier-panel .guest-tier-card.tier-95 { --tier-accent:#c084fc; --tier-soft:rgba(192,132,252,.10); --tier-border:rgba(192,132,252,.30); } +.arcade-dashboard-v2 .shared-tier-panel .guest-tier-card.tier-75 { --tier-accent:#3b82f6; --tier-soft:rgba(59,130,246,.10); --tier-border:rgba(59,130,246,.30); } +.arcade-dashboard-v2 .shared-tier-panel .guest-tier-card.tier-50 { --tier-accent:#34d399; --tier-soft:rgba(52,211,153,.10); --tier-border:rgba(52,211,153,.28); } +.arcade-dashboard-v2 .shared-tier-panel .guest-tier-card.is-current { + border-color: var(--tier-accent); + box-shadow: inset 3px 0 0 var(--tier-accent); +} +.arcade-dashboard-v2 .shared-tier-panel .guest-tier-icon { + display:grid; place-items:center; width:36px; height:36px; + border:1px solid var(--tier-border); border-radius:9px; + background:var(--tier-soft); color:var(--tier-accent); +} +.arcade-dashboard-v2 .shared-tier-panel .guest-tier-icon svg { width:17px; height:17px; } +.arcade-dashboard-v2 .shared-tier-panel .guest-tier-main { min-width:0; display:grid; gap:5px; } +.arcade-dashboard-v2 .shared-tier-panel .guest-tier-copy { min-width:0; display:grid; gap:1px; } +.arcade-dashboard-v2 .shared-tier-panel .guest-tier-copy strong { + overflow:hidden; color:var(--arcade-text); font-size:.72rem; font-weight:850; + text-overflow:ellipsis; text-transform:uppercase; white-space:nowrap; +} +.arcade-dashboard-v2 .shared-tier-panel .guest-tier-copy span { color:var(--arcade-muted); font-size:.61rem; } +.arcade-dashboard-v2 .shared-tier-panel .guest-tier-progress { + height:5px; overflow:hidden; border-radius:999px; background:rgba(100,116,139,.17); +} +.arcade-dashboard-v2 .shared-tier-panel .guest-tier-progress > span { + display:block; height:100%; border-radius:inherit; background:var(--tier-accent); +} +.arcade-dashboard-v2 .shared-tier-panel .guest-tier-availability { + min-width:72px; display:grid; justify-items:end; gap:2px; padding-left:7px; + border-left:1px solid var(--arcade-border-soft); text-align:right; +} +.arcade-dashboard-v2 .shared-tier-panel .guest-tier-availability > strong { + color:var(--arcade-text); font-size:.72rem; font-weight:850; font-variant-numeric:tabular-nums; +} +.arcade-dashboard-v2 .shared-tier-panel .guest-tier-availability > strong span { color:var(--arcade-muted); font-weight:600; } +.arcade-dashboard-v2 .shared-tier-panel .guest-tier-availability small { color:var(--arcade-muted); font-size:.55rem; } +.arcade-dashboard-v2 .shared-tier-panel .guest-tier-heading { display:flex; align-items:center; justify-content:space-between; gap:8px; margin:0; } +.arcade-dashboard-v2 .shared-tier-panel .guest-tier-heading > div { display:flex; align-items:center; gap:8px; min-width:0; } +.arcade-dashboard-v2 .shared-tier-panel .guest-tier-heading-icon { display:grid; place-items:center; width:28px; height:28px; flex:0 0 auto; } +.arcade-dashboard-v2 .shared-tier-panel .guest-tier-heading strong { font-size:.78rem; } +.arcade-dashboard-v2 .shared-tier-panel .guest-tier-heading span { font-size:.6rem; white-space:nowrap; } + /* Footer aligns with the workspace surfaces without competing with content. */ -.arcade-dashboard-v2 .arcade-footer { +.arcade-dashboard-v2[data-dashboard-view="guest"] .arcade-footer { margin-top: 34px; padding-top: 24px; border-top-color: var(--v2-border); @@ -688,34 +907,47 @@ html.light .arcade-dashboard-v2 .guest-dashboard-hero { /* Tablet */ @media (max-width: 1080px) { - .arcade-dashboard-v2 { + .arcade-dashboard-v2[data-dashboard-view="guest"] { --v2-gutter: 18px; } - .arcade-dashboard-v2 .dashboard-summary-grid { + .arcade-dashboard-v2[data-dashboard-view="guest"] .arcade-hero { + grid-template-columns: minmax(0, 0.9fr) minmax(340px, 1.1fr); + gap: 28px; + } + + .arcade-dashboard-v2[data-dashboard-view="guest"] .dashboard-summary-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); } - .arcade-dashboard-v2 .tier-status-panel { + .arcade-dashboard-v2[data-dashboard-view="guest"] .tier-status-panel { grid-column: 1 / -1; } - .arcade-dashboard-v2 .dashboard-content-grid { + .arcade-dashboard-v2[data-dashboard-view="guest"] .dashboard-content-grid { grid-template-columns: minmax(0, 1fr) 300px; } - .arcade-dashboard-v2 .earned-badge-grid { + .arcade-dashboard-v2[data-dashboard-view="guest"] .earned-badge-grid { grid-template-columns: repeat(3, minmax(0, 1fr)); } } /* Mobile / small tablet */ @media (max-width: 820px) { - .arcade-dashboard-v2 { + .arcade-dashboard-v2[data-dashboard-view="guest"] { --v2-gutter: 12px; } - .arcade-dashboard-v2 .arcade-nav { + .arcade-dashboard-v2[data-dashboard-view="guest"] .arcade-header { + top: 7px; + min-height: 58px; + margin-top: 7px; + padding-inline: 10px; + border-radius: 14px; + } + + .arcade-dashboard-v2[data-dashboard-view="guest"] .arcade-nav { top: 64px; left: 0; right: 0; @@ -724,30 +956,44 @@ html.light .arcade-dashboard-v2 .guest-dashboard-hero { background: var(--v2-surface-strong); } - .arcade-dashboard-v2 .arcade-nav a { + .arcade-dashboard-v2[data-dashboard-view="guest"] .arcade-nav a { padding: 12px; } - .arcade-dashboard-v2 .dashboard-content-grid { + .arcade-dashboard-v2[data-dashboard-view="guest"] .arcade-hero { grid-template-columns: 1fr; + gap: 26px; + padding: 42px 0 28px; } - .arcade-dashboard-v2 .tier-list-panel { + .arcade-dashboard-v2[data-dashboard-view="guest"] .hero-heading { + max-width: 650px; + } + + .arcade-dashboard-v2[data-dashboard-view="guest"] .profile-analyzer-card { + padding: 22px; + } + + .arcade-dashboard-v2[data-dashboard-view="guest"] .dashboard-content-grid { + grid-template-columns: 1fr; + } + + .arcade-dashboard-v2[data-dashboard-view="guest"] .tier-list-panel { order: 0; } - .arcade-dashboard-v2 .dashboard-bottom-grid { + .arcade-dashboard-v2[data-dashboard-view="guest"] .dashboard-bottom-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); } - .arcade-dashboard-v2 .next-goal-panel { + .arcade-dashboard-v2[data-dashboard-view="guest"] .next-goal-panel { grid-column: 1 / -1; } } /* Phones */ @media (max-width: 600px) { - .arcade-dashboard-v2 { + .arcade-dashboard-v2[data-dashboard-view="guest"] { --v2-gutter: 8px; padding-bottom: calc(70px + env(safe-area-inset-bottom)); } @@ -756,19 +1002,66 @@ html.light .arcade-dashboard-v2 .guest-dashboard-hero { padding-bottom: calc(70px + env(safe-area-inset-bottom)); } + .arcade-dashboard-v2[data-dashboard-view="guest"] .arcade-header { + width: calc(100% - 16px); + } + + .arcade-dashboard-v2[data-dashboard-view="guest"] .arcade-brand-mark { + width: 32px; + height: 32px; + } + + .arcade-dashboard-v2[data-dashboard-view="guest"] .arcade-brand-copy { + font-size: 0.58rem; + } + + .arcade-dashboard-v2[data-dashboard-view="guest"] .arcade-hero { + padding-top: 34px; + } + + .arcade-dashboard-v2[data-dashboard-view="guest"] .pixel-kicker { + padding: 6px 8px; + font-size: 0.62rem; + } + + .arcade-dashboard-v2[data-dashboard-view="guest"] .hero-heading h1 { + margin-top: 18px; + font-size: clamp(1.8rem, 9.2vw, 2.6rem); + line-height: 1.25; + } + + .arcade-dashboard-v2[data-dashboard-view="guest"] .hero-description { + font-size: 0.88rem; + } + + .arcade-dashboard-v2[data-dashboard-view="guest"] .trust-pills { + display: flex; + flex-direction: column; + align-items: flex-start; + } + + .arcade-dashboard-v2[data-dashboard-view="guest"] .trust-pills span { + width: fit-content; + } + + .arcade-dashboard-v2[data-dashboard-view="guest"] .profile-analyzer-card { + padding: 16px; + border-radius: 16px; + } + .arcade-dashboard-v2 .preview-mode-toolbar { width: calc(100% - 32px); max-width: 360px; } - .arcade-dashboard-v2 .dashboard-empty-state.guest-dashboard { + .arcade-dashboard-v2[data-dashboard-view="guest"] .dashboard-empty-state.guest-dashboard { gap: 16px; padding: 0; margin-bottom: 16px; border-radius: 18px; } - .arcade-dashboard-v2 .guest-dashboard-hero { + .arcade-dashboard-v2[data-dashboard-view="guest"] .guest-dashboard-hero { grid-template-columns: 64px minmax(0, 1fr); gap: 14px; min-height: 0; @@ -776,7 +1069,7 @@ html.light .arcade-dashboard-v2 .guest-dashboard-hero { border-radius: 16px; } - .arcade-dashboard-v2 .guest-dashboard-orb { + .arcade-dashboard-v2[data-dashboard-view="guest"] .guest-dashboard-orb { width: 58px; height: 58px; box-shadow: @@ -784,48 +1077,48 @@ html.light .arcade-dashboard-v2 .guest-dashboard-hero { 0 0 24px rgba(255, 193, 7, 0.12); } - .arcade-dashboard-v2 .guest-dashboard-orb svg { + .arcade-dashboard-v2[data-dashboard-view="guest"] .guest-dashboard-orb svg { width: 26px; height: 26px; } - .arcade-dashboard-v2 .guest-dashboard-copy { + .arcade-dashboard-v2[data-dashboard-view="guest"] .guest-dashboard-copy { gap: 7px; } - .arcade-dashboard-v2 .guest-dashboard-copy strong { + .arcade-dashboard-v2[data-dashboard-view="guest"] .guest-dashboard-copy strong { font-size: 1.05rem; line-height: 1.25; } - .arcade-dashboard-v2 .guest-dashboard-copy p { + .arcade-dashboard-v2[data-dashboard-view="guest"] .guest-dashboard-copy p { font-size: 0.76rem; line-height: 1.5; } - .arcade-dashboard-v2 .guest-dashboard-decoration { + .arcade-dashboard-v2[data-dashboard-view="guest"] .guest-dashboard-decoration { display: none; } - .arcade-dashboard-v2 .guest-tier-heading { + .arcade-dashboard-v2[data-dashboard-view="guest"] .guest-tier-heading { margin-top: 0; } - .arcade-dashboard-v2 .guest-tier-heading-icon { + .arcade-dashboard-v2[data-dashboard-view="guest"] .guest-tier-heading-icon { width: 32px; height: 32px; } - .arcade-dashboard-v2 .guest-tier-heading strong { + .arcade-dashboard-v2[data-dashboard-view="guest"] .guest-tier-heading strong { font-size: 0.8rem; } - .arcade-dashboard-v2 .empty-tier-grid { + .arcade-dashboard-v2[data-dashboard-view="guest"] .empty-tier-grid { grid-template-columns: 1fr; gap: 8px; } - .arcade-dashboard-v2 .guest-tier-card { + .arcade-dashboard-v2[data-dashboard-view="guest"] .guest-tier-card { grid-template-columns: 46px minmax(0, 1fr) auto; gap: 11px; min-height: 88px; @@ -833,123 +1126,152 @@ html.light .arcade-dashboard-v2 .guest-dashboard-hero { border-radius: 14px; } - .arcade-dashboard-v2 .guest-tier-icon { + .arcade-dashboard-v2[data-dashboard-view="guest"] .guest-tier-icon { width: 44px; height: 44px; border-radius: 11px; } - .arcade-dashboard-v2 .guest-tier-icon svg { + .arcade-dashboard-v2[data-dashboard-view="guest"] .guest-tier-icon svg { width: 21px; height: 21px; } - .arcade-dashboard-v2 .guest-tier-main { + .arcade-dashboard-v2[data-dashboard-view="guest"] .guest-tier-main { gap: 8px; } - .arcade-dashboard-v2 .guest-tier-copy strong { + .arcade-dashboard-v2[data-dashboard-view="guest"] .guest-tier-copy strong { font-size: 0.85rem; } - .arcade-dashboard-v2 .guest-tier-copy span { + .arcade-dashboard-v2[data-dashboard-view="guest"] .guest-tier-copy span { font-size: 0.70rem; } - .arcade-dashboard-v2 .guest-tier-progress { + .arcade-dashboard-v2[data-dashboard-view="guest"] .guest-tier-progress { height: 6px; } - .arcade-dashboard-v2 .guest-tier-availability { + .arcade-dashboard-v2[data-dashboard-view="guest"] .guest-tier-availability { min-width: 94px; gap: 2px; padding-left: 10px; } - .arcade-dashboard-v2 .guest-tier-availability > strong { + .arcade-dashboard-v2[data-dashboard-view="guest"] .guest-tier-availability > strong { font-size: 0.85rem; } - .arcade-dashboard-v2 .guest-tier-availability small { + .arcade-dashboard-v2[data-dashboard-view="guest"] .guest-tier-availability small { max-width: 88px; font-size: 0.66rem; } - .arcade-dashboard-v2 .extension-strip { + .arcade-dashboard-v2[data-dashboard-view="guest"] .profile-input { + min-height: 52px; + } + + .arcade-dashboard-v2[data-dashboard-view="guest"] .analyze-button { + min-height: 48px; + } + + .arcade-dashboard-v2[data-dashboard-view="guest"] .extension-strip { grid-template-columns: auto 1fr; padding: 12px; border-radius: 14px; } - .arcade-dashboard-v2 .extension-store-actions { + .arcade-dashboard-v2[data-dashboard-view="guest"] .extension-store-actions { grid-column: 1 / -1; } - .arcade-dashboard-v2 .dashboard-summary-grid, -.arcade-dashboard-v2 .dashboard-bottom-grid { + .arcade-dashboard-v2[data-dashboard-view="guest"] .dashboard-summary-grid, + .arcade-dashboard-v2[data-dashboard-view="guest"] .dashboard-bottom-grid { grid-template-columns: 1fr; } - .arcade-dashboard-v2 .tier-status-panel, -.arcade-dashboard-v2 .next-goal-panel { + .arcade-dashboard-v2[data-dashboard-view="guest"] .tier-status-panel, + .arcade-dashboard-v2[data-dashboard-view="guest"] .next-goal-panel { grid-column: auto; } - .arcade-dashboard-v2 .dashboard-panel { + .arcade-dashboard-v2[data-dashboard-view="guest"] .dashboard-panel { padding: 16px; border-radius: 14px; } - .arcade-dashboard-v2 .profile-overview { + .arcade-dashboard-v2[data-dashboard-view="guest"] .profile-overview { gap: 12px; margin: 16px 0; } - .arcade-dashboard-v2 .profile-photo { + .arcade-dashboard-v2[data-dashboard-view="guest"] .profile-photo { width: 62px; height: 62px; } - .arcade-dashboard-v2 .profile-stat-grid { + .arcade-dashboard-v2[data-dashboard-view="guest"] .profile-stat-grid { gap: 6px; } - .arcade-dashboard-v2 .profile-stat-grid > div { + .arcade-dashboard-v2[data-dashboard-view="guest"] .profile-stat-grid > div { min-height: 72px; padding: 8px 5px; } - .arcade-dashboard-v2 .profile-stat-icon { + .arcade-dashboard-v2[data-dashboard-view="guest"] .profile-stat-icon { width: 26px; height: 26px; } - .arcade-dashboard-v2 .points-total strong { + .arcade-dashboard-v2[data-dashboard-view="guest"] .points-total strong { font-size: 2.05rem; } - .arcade-dashboard-v2 .point-breakdown-row { + .arcade-dashboard-v2[data-dashboard-view="guest"] .point-breakdown-row { grid-template-columns: minmax(0, 1fr) auto 45px; gap: 7px; } - .arcade-dashboard-v2 .earned-badge-grid { + .arcade-dashboard-v2[data-dashboard-view="guest"] .earned-badge-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); } + + .arcade-dashboard-v2[data-dashboard-view="guest"] .mobile-bottom-nav { + left: 0; + right: 0; + bottom: 0; + min-height: 54px; + padding: 5px 8px calc(5px + env(safe-area-inset-bottom)); + border: 0; + border-top: 1px solid var(--v2-border); + border-radius: 0; + background: var(--v2-surface-strong); + box-shadow: var(--v2-shadow); + } + + .arcade-dashboard-v2[data-dashboard-view="guest"] .mobile-bottom-nav a { + border-radius: 10px; + } + + .arcade-dashboard-v2[data-dashboard-view="guest"] .mobile-bottom-nav a:active { + background: rgba(139, 92, 246, 0.11); + } } @media (max-width: 380px) { - .arcade-dashboard-v2 .earned-badge-grid { + .arcade-dashboard-v2[data-dashboard-view="guest"] .earned-badge-grid { grid-template-columns: 1fr; } - .arcade-dashboard-v2 .header-store-link { + .arcade-dashboard-v2[data-dashboard-view="guest"] .header-store-link { display: none; } } /* The same cards also fit the narrower desktop profile sidebar. */ -.arcade-dashboard-v2 .shared-tier-panel { +.arcade-dashboard-v2[data-dashboard-view="guest"] .shared-tier-panel { container-type: inline-size; display: flex; flex-direction: column; @@ -959,47 +1281,47 @@ html.light .arcade-dashboard-v2 .guest-dashboard-hero { } @container (max-width: 420px) { - .arcade-dashboard-v2 .guest-tier-card { + .arcade-dashboard-v2[data-dashboard-view="guest"] .guest-tier-card { grid-template-columns: 44px minmax(0, 1fr) auto; gap: 8px; min-height: 88px; padding: 11px 12px; } - .arcade-dashboard-v2 .guest-tier-icon { + .arcade-dashboard-v2[data-dashboard-view="guest"] .guest-tier-icon { width: 44px; height: 44px; } - .arcade-dashboard-v2 .guest-tier-copy strong { + .arcade-dashboard-v2[data-dashboard-view="guest"] .guest-tier-copy strong { font-size: 0.85rem; } - .arcade-dashboard-v2 .guest-tier-copy span { + .arcade-dashboard-v2[data-dashboard-view="guest"] .guest-tier-copy span { font-size: 0.70rem; } - .arcade-dashboard-v2 .guest-tier-availability { + .arcade-dashboard-v2[data-dashboard-view="guest"] .guest-tier-availability { min-width: 94px; padding-left: 8px; } - .arcade-dashboard-v2 .guest-tier-availability > strong { + .arcade-dashboard-v2[data-dashboard-view="guest"] .guest-tier-availability > strong { font-size: 0.85rem; } - .arcade-dashboard-v2 .guest-tier-availability small { + .arcade-dashboard-v2[data-dashboard-view="guest"] .guest-tier-availability small { max-width: 88px; font-size: 0.66rem; } } /* Consistent section rhythm, without extra boxes around supporting copy. */ -.arcade-dashboard-v2 .shared-tier-panel .guest-tier-heading { +.arcade-dashboard-v2[data-dashboard-view="guest"] .shared-tier-panel .guest-tier-heading { margin: 0; } -.arcade-dashboard-v2 .shared-tier-panel .tier-note { +.arcade-dashboard-v2[data-dashboard-view="guest"] .shared-tier-panel .tier-note { margin: 0; padding: 0; border: 0; @@ -1010,12 +1332,12 @@ html.light .arcade-dashboard-v2 .guest-dashboard-hero { line-height: 1.65; } -.arcade-dashboard-v2 .shared-tier-panel .tier-trends-trigger { +.arcade-dashboard-v2[data-dashboard-view="guest"] .shared-tier-panel .tier-trends-trigger { margin: 0; box-shadow: none; } -.arcade-dashboard-v2 .shared-tier-panel .swag-drops-preview { +.arcade-dashboard-v2[data-dashboard-view="guest"] .shared-tier-panel .swag-drops-preview { margin-top: 4px; } @@ -1026,15 +1348,15 @@ html.light .arcade-dashboard-v2 .guest-dashboard-hero { } /* New tier surfaces use the Font Awesome adapter, not only SVG icons. */ -.arcade-dashboard-v2 .guest-tier-icon .fa-icon { +.arcade-dashboard-v2[data-dashboard-view="guest"] .guest-tier-icon .fa-icon { font-size: 20px; } -.arcade-dashboard-v2 .guest-tier-heading-icon .fa-icon { +.arcade-dashboard-v2[data-dashboard-view="guest"] .guest-tier-heading-icon .fa-icon { font-size: 17px; } -.arcade-dashboard-v2 .guest-dashboard-orb .fa-icon { +.arcade-dashboard-v2[data-dashboard-view="guest"] .guest-dashboard-orb .fa-icon { font-size: 28px; } @@ -1043,13 +1365,13 @@ html.light .arcade-dashboard-v2 .guest-dashboard-hero { } /* Tier accents remain readable on the light surfaces. */ -html.light .arcade-dashboard-v2 .guest-tier-card.tier-120 { --tier-accent: #a16207; } -html.light .arcade-dashboard-v2 .guest-tier-card.tier-95 { --tier-accent: #9333ea; } -html.light .arcade-dashboard-v2 .guest-tier-card.tier-75 { --tier-accent: #2563eb; } -html.light .arcade-dashboard-v2 .guest-tier-card.tier-50 { --tier-accent: #059669; } +html.light .arcade-dashboard-v2[data-dashboard-view="guest"] .guest-tier-card.tier-120 { --tier-accent: #a16207; } +html.light .arcade-dashboard-v2[data-dashboard-view="guest"] .guest-tier-card.tier-95 { --tier-accent: #9333ea; } +html.light .arcade-dashboard-v2[data-dashboard-view="guest"] .guest-tier-card.tier-75 { --tier-accent: #2563eb; } +html.light .arcade-dashboard-v2[data-dashboard-view="guest"] .guest-tier-card.tier-50 { --tier-accent: #059669; } /* Keep the attained rank readable in the compact profile summary. */ -.arcade-dashboard-v2 .profile-stat-grid > div:last-child strong { +.arcade-dashboard-v2[data-dashboard-view="guest"] .profile-stat-grid > div:last-child strong { overflow: visible; white-space: normal; text-overflow: clip; From 1fc30c68a0967ae6bf5c9cf0a4e521e86e5ee30a Mon Sep 17 00:00:00 2001 From: David Nguyen Date: Mon, 5 Oct 2026 11:53:22 +0700 Subject: [PATCH 13/27] sync: inherit app/styles/site-ui.css from #89 --- app/styles/site-ui.css | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/app/styles/site-ui.css b/app/styles/site-ui.css index 30cec31c4..7e918a2a5 100644 --- a/app/styles/site-ui.css +++ b/app/styles/site-ui.css @@ -32,11 +32,21 @@ html:is(.light, .dark) .arcade-dashboard-page.arcade-dashboard-page { .website-language-menu.website-language-menu { border-color: var(--ui-border); border-radius: var(--ui-radius); background: var(--ui-surface); color: var(--ui-text); box-shadow: var(--ui-shadow); max-height: min(480px, calc(100dvh - 110px)); overflow-y: auto; } .website-language-option.website-language-option { color: var(--ui-text); border-radius: var(--ui-radius-sm); } .website-language-option:is(:hover,.is-selected) { background: var(--ui-accent-soft); } -.arcade-dashboard-page .site-footer.site-footer { display: flex; flex-direction: column; align-items: flex-start; gap: 16px; width: min(var(--ui-width), calc(100% - var(--ui-gutter) * 2)); margin: 32px auto 0; padding: 24px 0 32px; border-top: 1px solid var(--ui-border); background: transparent; } -.site-footer p { max-width: 68ch; margin: 0; font-size: .85rem; color: var(--ui-muted); } -.site-footer .footer-route-links { display: flex; flex-wrap: wrap; gap: 12px 24px; } -.site-footer .footer-route-links a { color: var(--ui-muted); font-size: .85rem; } -.site-footer .footer-route-links a:hover { color: var(--ui-accent); } +.arcade-dashboard-page .site-footer.site-footer { + width: min(1280px, calc(100% - 40px)); + margin: 32px auto 0; + background: transparent; +} +.site-footer .footer-brand { flex: 0 0 auto; } +.site-footer p { margin: 0; color: var(--arcade-muted); } +.site-footer .footer-route-links { display: flex; flex-wrap: wrap; gap: 12px 18px; } +.site-footer .footer-route-links a, +.site-footer .footer-store-links a { color: var(--arcade-muted); text-decoration: none; } +.site-footer .footer-route-links a:hover, +.site-footer .footer-store-links a:hover { color: var(--arcade-text); } +.site-footer .footer-store-links { display: flex; align-items: center; gap: 12px; } +.site-footer .footer-store-links a { display: inline-flex; align-items: center; gap: 5px; } +.site-footer .footer-store-links svg { width: 14px; height: 14px; } /* Readable content and Swag grids share the same container and hierarchy. */ .site-content-page .internal-page-hero { border: 0; background: transparent; } .site-content-page .internal-page-glow { display: none; } From 2c1d803459982afeeda97393ce6db9078c3b946c Mon Sep 17 00:00:00 2001 From: David Nguyen Date: Mon, 5 Oct 2026 11:53:26 +0700 Subject: [PATCH 14/27] sync: inherit components/back-to-top.tsx from #89 --- components/back-to-top.tsx | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 components/back-to-top.tsx diff --git a/components/back-to-top.tsx b/components/back-to-top.tsx new file mode 100644 index 000000000..67cb7020a --- /dev/null +++ b/components/back-to-top.tsx @@ -0,0 +1,31 @@ +"use client" + +import { ArrowUp } from "lucide-react" +import { useEffect, useState } from "react" + +const SHOW_AFTER = 560 + +export default function BackToTop() { + const [visible, setVisible] = useState(false) + + useEffect(() => { + const update = () => setVisible(window.scrollY > SHOW_AFTER) + update() + window.addEventListener("scroll", update, { passive: true }) + return () => window.removeEventListener("scroll", update) + }, []) + + if (!visible) return null + + return ( + + ) +} From 66517b19e912ff273796cc809ed03fee6f5ecfac Mon Sep 17 00:00:00 2001 From: David Nguyen Date: Mon, 5 Oct 2026 11:53:29 +0700 Subject: [PATCH 15/27] sync: inherit components/site/site-footer.tsx from #89 --- components/site/site-footer.tsx | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/components/site/site-footer.tsx b/components/site/site-footer.tsx index cbbecb78c..c0ba80152 100644 --- a/components/site/site-footer.tsx +++ b/components/site/site-footer.tsx @@ -1,12 +1,18 @@ "use client" import Link from "next/link" +import { Chrome, Gamepad2, Globe2 } from "lucide-react" +import { CHROME_EXTENSION_URL, FIREFOX_EXTENSION_URL } from "@/components/arcade/model" import { useSiteMessages } from "./use-site-messages" export default function SiteFooter() { const messages = useSiteMessages() return (
+
+ + ARCADEPOINTS +

{messages.unofficial}

+
) } From 206c2e655d4f63e6ed5a7fc9eebea3eada8eb34a Mon Sep 17 00:00:00 2001 From: David Nguyen Date: Mon, 5 Oct 2026 11:53:42 +0700 Subject: [PATCH 16/27] sync: inherit dashboard regression tests from #89 --- tests/home-layout.test.mjs | 22 +++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/tests/home-layout.test.mjs b/tests/home-layout.test.mjs index 7b6af8a9e..6587aba42 100644 --- a/tests/home-layout.test.mjs +++ b/tests/home-layout.test.mjs @@ -51,7 +51,7 @@ test("Monthly Labs has deterministic React-owned anchors in both dashboard state assert.match(guestDashboard, / emptyMonthly) - assert.match(readRepoFile("components/site/site-header.tsx"), /href="\/monthly-labs\/"/) + assert.match(readRepoFile("components/site/site-header.tsx"), /href="\\/monthly-labs\\/"/) assert.match(guide, /data-home-order="about"/) }) @@ -105,7 +105,14 @@ test("home dashboard v2 stays scoped and its visual overrides load last", () => assert.match(calculator, /arcade-dashboard-page arcade-dashboard-v2/) assert.match(refactorStyles, /\.arcade-dashboard-v2/) - assert.match(refactorStyles, /\.arcade-dashboard-v2 \.program-countdown-host/) + assert.match( + refactorStyles, + /\.arcade-dashboard-v2\[data-dashboard-view="guest"\] \.program-countdown-host/, + ) + assert.doesNotMatch( + refactorStyles, + /\.arcade-dashboard-v2 \.program-countdown-host/, + ) assert.match(refactorStyles, /\.guest-dashboard-hero/) assert.match(refactorStyles, /\.guest-tier-card\.tier-120/) assert.match(refactorStyles, /\.guest-tier-progress/) @@ -185,12 +192,13 @@ test("PR-preview guest and profile debug modes stay profile-safe and fully local }) -test("guest and profile share one tier renderer; only profile passes the attained tier", () => { - assert.equal((calculator.match(/ { + assert.equal((calculator.match(/ Date: Mon, 5 Oct 2026 11:53:45 +0700 Subject: [PATCH 17/27] sync: reconcile #89 layout with #90 chatbot --- app/layout.tsx | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/app/layout.tsx b/app/layout.tsx index 8134f4be1..03e5695c8 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -2,9 +2,10 @@ import { readFileSync } from "node:fs" import path from "node:path" import localFont from "next/font/local" import type { Metadata, Viewport } from "next" -import WebsiteLanguage from "@/components/i18n/website-language" import ArcadeFaqChatbot from "@/components/arcade/arcade-faq-chatbot" +import WebsiteLanguage from "@/components/i18n/website-language" import CookieConsent from "@/components/privacy/cookie-consent" +import BackToTop from "@/components/back-to-top" import { ThemeProvider } from "@/components/theme-provider" import ThemeToggle from "@/components/theme-toggle" import { WEBSITE_SITE_URL } from "@/lib/website-i18n" @@ -30,10 +31,7 @@ import "./styles/facilitator-participation.css" import "./styles/facilitator-launcher-visibility.css" import "./styles/cookie-consent.css" import "./styles/home-refactor.css" -import "./styles/shared-profile.css" -import "./styles/profile-share.css" -import "./styles/bonus-milestone.css" -import "./styles/site-ui.css" +import "./styles/back-to-top.css" import "./styles/arcade-chatbot.css" const arcadeRequestDedupeBootstrap = readFileSync( @@ -242,6 +240,7 @@ export default function RootLayout({ children }: Readonly<{ children: React.Reac {children} + Date: Mon, 5 Oct 2026 11:55:50 +0700 Subject: [PATCH 18/27] sync: inherit latest components/site/site-footer.tsx from #89 --- components/site/site-footer.tsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/components/site/site-footer.tsx b/components/site/site-footer.tsx index c0ba80152..f983898fb 100644 --- a/components/site/site-footer.tsx +++ b/components/site/site-footer.tsx @@ -2,9 +2,13 @@ import Link from "next/link" import { Chrome, Gamepad2, Globe2 } from "lucide-react" -import { CHROME_EXTENSION_URL, FIREFOX_EXTENSION_URL } from "@/components/arcade/model" import { useSiteMessages } from "./use-site-messages" +const CHROME_EXTENSION_URL = + "https://chromewebstore.google.com/detail/google-cloud-skills-boost/lmbhjioadhcoebhgapaidogodllonbgg" +const FIREFOX_EXTENSION_URL = + "https://addons.mozilla.org/addon/cloud-skills-boost-helper" + export default function SiteFooter() { const messages = useSiteMessages() return ( From 65b7ffa2a02f56ab69bcd95450884c1e3fc738f0 Mon Sep 17 00:00:00 2001 From: David Nguyen Date: Mon, 5 Oct 2026 11:55:54 +0700 Subject: [PATCH 19/27] sync: inherit latest tests/home-layout.test.mjs from #89 From c8823f6ffe193a4e7673b7b8fed10da0d3194af8 Mon Sep 17 00:00:00 2001 From: David Nguyen Date: Mon, 5 Oct 2026 11:55:58 +0700 Subject: [PATCH 20/27] sync: keep chatbot on latest #89 layout --- app/layout.tsx | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/layout.tsx b/app/layout.tsx index 03e5695c8..a9ba17709 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -31,6 +31,10 @@ import "./styles/facilitator-participation.css" import "./styles/facilitator-launcher-visibility.css" import "./styles/cookie-consent.css" import "./styles/home-refactor.css" +import "./styles/shared-profile.css" +import "./styles/profile-share.css" +import "./styles/bonus-milestone.css" +import "./styles/site-ui.css" import "./styles/back-to-top.css" import "./styles/arcade-chatbot.css" From 56410a71215ce8bb5c08fab456e5c387a0a53aa7 Mon Sep 17 00:00:00 2001 From: David Nguyen Date: Mon, 5 Oct 2026 11:57:00 +0700 Subject: [PATCH 21/27] sync: inherit latest dashboard test --- tests/home-layout.test.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/home-layout.test.mjs b/tests/home-layout.test.mjs index 6587aba42..e18cc8048 100644 --- a/tests/home-layout.test.mjs +++ b/tests/home-layout.test.mjs @@ -51,7 +51,7 @@ test("Monthly Labs has deterministic React-owned anchors in both dashboard state assert.match(guestDashboard, / emptyMonthly) - assert.match(readRepoFile("components/site/site-header.tsx"), /href="\\/monthly-labs\\/"/) + assert.ok(readRepoFile("components/site/site-header.tsx").includes('href="/monthly-labs/"')) assert.match(guide, /data-home-order="about"/) }) From 7405570fdf5eeb3970aa9d367551b76ab736c23c Mon Sep 17 00:00:00 2001 From: David Nguyen Date: Mon, 5 Oct 2026 19:23:31 +0700 Subject: [PATCH 22/27] fix: sync facilitator season state handling --- .../arcade/facilitator-participation.ts | 45 +++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/components/arcade/facilitator-participation.ts b/components/arcade/facilitator-participation.ts index ee284354d..2be2feed9 100644 --- a/components/arcade/facilitator-participation.ts +++ b/components/arcade/facilitator-participation.ts @@ -1,6 +1,19 @@ export const FACILITATOR_PARTICIPATION_EVENT = "arcade-facilitator-participation-change" export const FACILITATOR_PANEL_OPEN_EVENT = "arcade-facilitator-panel-open" +export const FACILITATOR_PROGRAM_STATE_EVENT = + "arcade-facilitator-program-state-change" + +export type FacilitatorProgramState = + | "unknown" + | "active" + | "ended" + | "unconfigured" + | "disabled" + +export type FacilitatorProgramStateDetail = { + state: FacilitatorProgramState +} const PARTICIPATION_STORAGE_PREFIX = "arcade-facilitator-participation-v1" @@ -61,3 +74,35 @@ export function writeFacilitatorParticipation( ), ) } + +const FACILITATOR_PROGRAM_STATES = new Set([ + "unknown", + "active", + "ended", + "unconfigured", + "disabled", +]) + +export function readFacilitatorProgramState(): FacilitatorProgramState { + if (typeof document === "undefined") return "unknown" + + const state = document.documentElement.dataset.facilitatorProgramState + return FACILITATOR_PROGRAM_STATES.has(state as FacilitatorProgramState) + ? (state as FacilitatorProgramState) + : "unknown" +} + +export function writeFacilitatorProgramState( + state: FacilitatorProgramState, +): void { + if (typeof document === "undefined" || typeof window === "undefined") return + if (document.documentElement.dataset.facilitatorProgramState === state) return + + document.documentElement.dataset.facilitatorProgramState = state + window.dispatchEvent( + new CustomEvent( + FACILITATOR_PROGRAM_STATE_EVENT, + { detail: { state } }, + ), + ) +} From 88239c3fefc77271f7e035ff0ba569a3e515a46e Mon Sep 17 00:00:00 2001 From: David Nguyen Date: Mon, 5 Oct 2026 19:23:34 +0700 Subject: [PATCH 23/27] fix: sync facilitator season state handling --- components/arcade/program-countdown.tsx | 35 ++++++++++++++++++++----- 1 file changed, 29 insertions(+), 6 deletions(-) diff --git a/components/arcade/program-countdown.tsx b/components/arcade/program-countdown.tsx index 738f2ff88..6f1964026 100644 --- a/components/arcade/program-countdown.tsx +++ b/components/arcade/program-countdown.tsx @@ -9,10 +9,14 @@ import { type DeadlineSource, type ResolvedDeadline, } from "./countdown-deadline" +import { + FACILITATOR_PANEL_OPEN_EVENT, + writeFacilitatorProgramState, + type FacilitatorProgramState, +} from "./facilitator-participation" const HOST_SELECTOR = ".program-countdown-host" const HERO_SELECTOR = ".arcade-hero" -const FACILITATOR_LAUNCHER_SELECTOR = ".facilitator-launcher" const DEFAULT_TIME_ZONE_OFFSET = "+05:30" const FIREBASE_CDN_VERSION = "12.18.0" const DEFAULT_FETCH_INTERVAL_MS = 900_000 @@ -339,10 +343,18 @@ function formatPart(value: number): string { return String(value).padStart(2, "0") } +function facilitatorProgramState( + config: ProgramCountdownConfig | undefined, + nowMs: number, +): FacilitatorProgramState { + if (!config) return "unknown" + if (!config.enabled) return "disabled" + if (!config.deadline) return "unconfigured" + return countdownParts(config.deadline, nowMs).ended ? "ended" : "active" +} + function openFacilitatorDetails() { - document - .querySelector(FACILITATOR_LAUNCHER_SELECTOR) - ?.click() + window.dispatchEvent(new Event(FACILITATOR_PANEL_OPEN_EVENT)) } function ProgramCard({ @@ -389,8 +401,12 @@ function ProgramCard({ ) : remaining.ended ? (
- Unavailable - Program tracker + {config.id === "facilitator" ? "Season ended" : "Unavailable"} + + {config.id === "facilitator" + ? "Facilitator Program 2026 has ended" + : "Program tracker"} +
{config.id === "facilitator" ? (