diff --git a/demos/guided-ui/README.md b/demos/guided-ui/README.md
new file mode 100644
index 00000000..e0751167
--- /dev/null
+++ b/demos/guided-ui/README.md
@@ -0,0 +1,24 @@
+# Guided UI | WebMCP Demo
+
+This demo illustrates a "Guided UI" using Vanilla JavaScript and the WebMCP protocol. The page features a dynamic, state-aware dashboard. Instead of a static tutorial, an AI Agent uses WebMCP tools to visually guide the user through a multi-step workflow based on their specific goal.
+
+## Features
+
+- **Dynamic Discovery Dashboard**: A mock "cluttered" UI with 10 different action buttons. Crucially, most buttons start hidden or locked. Completing prerequisite actions reveals new paths in the UI.
+- **Goal Input**: A text area where users can specify their ultimate goal (e.g., "I want to clean the data and generate a report").
+- **State-Aware Guidance**: The AI Agent dynamically analyzes the user's goal and the *currently available* dashboard state using the `get_available_actions` tool. It must reason through the chain of commands to unlock the required features.
+- **Synchronous WebMCP Tools**: The `highlight_ui_element` tool uses Promises to wait for the user to click the highlighted button before returning to the Agent. This enables a seamless loop where the Agent guides the user step-by-step, re-evaluating the newly revealed actions after each interaction until the ultimate goal is achieved.
+- **Vanilla JS Foundations**: The entire experience is driven by Vanilla JS and CSS transitions, showing how robust WebMCP implementations can be without heavy frameworks.
+
+## How it works
+
+1. The user types their goal into the input field.
+2. The user asks the WebMCP agent (e.g. via the WebMCP inspector extension) to guide them.
+3. The Agent calls the `get_available_actions` tool to understand what buttons are currently visible in the DOM and what the user's goal is.
+4. The Agent reasons out the next required step (even if the final button is hidden, it must know to trigger the visible prerequisite).
+5. The Agent calls the `highlight_ui_element` tool with the corresponding element ID and an instruction.
+6. The webpage darkens the background and highlights the targeted button, fading it into focus and showing a tooltip with the Agent's instruction.
+7. Execution of the tool is paused until the user clicks the highlighted button.
+8. Upon clicking, the underlying JS toggles the button state and dynamically fades in any newly unlocked dependent buttons.
+9. The `highlight_ui_element` tool resolves, explicitly telling the Agent to call `get_available_actions` again.
+10. The loop continues until the user's ultimate goal is fulfilled.
\ No newline at end of file
diff --git a/demos/guided-ui/index.html b/demos/guided-ui/index.html
new file mode 100644
index 00000000..2b8d74c6
--- /dev/null
+++ b/demos/guided-ui/index.html
@@ -0,0 +1,136 @@
+
+
+
+
+
+
+
+ Guided UI | WebMCP Demo
+
+
+
+
+
+
+
+
+ โ ๏ธ WebMCP is not detected. Please run this demo in a compatible browser or extension to use the AI Agent guidance.
+
+
+
+
+ What is your Ultimate Goal?
+
+
+
+ Quick Suggestions:
+ I need to connect to our live server, clean up the data, and email the report.
+ I want to start a brand new project, initialize the database, and export a PDF.
+ I'm in a hurry. I just want to use a template to generate a PDF report quickly.
+ I just need to fetch the schema from a new project. Stop after that.
+
+
+
+ โณ
+ WebMCP Agent is thinking...
+
+
+ Enter or select your goal above, then ask your WebMCP agent to guide you!
+
+
+
+
+
+
+
+
+ โจ New Project
+
+
+
+ ๐ Connect to Server
+
+
+
+ ๐ฅ Import Template
+
+
+
+
+
+ ๐๏ธ Initialize Database
+
+
+
+
+
+ ๐ Enter Credentials
+
+
+
+
+
+ ๐ Select Template
+
+
+
+ ๐จ Customize Template
+
+
+
+
+
+ ๐ Fetch Schema
+
+
+
+ ๐งน Clean Data
+
+
+
+ ๐ Join Tables
+
+
+
+ ๐ Apply Filter
+
+
+
+ ๐ Analyze Trends
+
+
+
+ ๐ Generate Report
+
+
+
+ ๐พ Export PDF
+
+
+
+ โ๏ธ Send Email
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/demos/guided-ui/script.js b/demos/guided-ui/script.js
new file mode 100644
index 00000000..a45c6093
--- /dev/null
+++ b/demos/guided-ui/script.js
@@ -0,0 +1,366 @@
+/**
+ * Copyright 2026 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+// Application State
+const state = {
+ newProject: false,
+ connectServer: false,
+ importTemplate: false,
+ dbInitialized: false,
+ credentialsEntered: false,
+ templateSelected: false,
+ templateCustomized: false,
+ schemaFetched: false,
+ dataCleaned: false,
+ tablesJoined: false,
+ filterApplied: false,
+ trendsAnalyzed: false,
+ reportGenerated: false,
+ pdfExported: false,
+ emailSent: false,
+};
+
+// Map DOM IDs to state properties
+const buttonToStateMap = {
+ 'btn-new-project': 'newProject',
+ 'btn-connect-server': 'connectServer',
+ 'btn-import-template': 'importTemplate',
+ 'btn-init-db': 'dbInitialized',
+ 'btn-enter-credentials': 'credentialsEntered',
+ 'btn-select-template': 'templateSelected',
+ 'btn-customize-template': 'templateCustomized',
+ 'btn-fetch-schema': 'schemaFetched',
+ 'btn-clean-data': 'dataCleaned',
+ 'btn-join-tables': 'tablesJoined',
+ 'btn-apply-filter': 'filterApplied',
+ 'btn-analyze-trends': 'trendsAnalyzed',
+ 'btn-generate-report': 'reportGenerated',
+ 'btn-export-pdf': 'pdfExported',
+ 'btn-send-email': 'emailSent',
+};
+
+// Workflow tracking to prevent cross-contamination
+let currentWorkflow = null; // 'new_project', 'connect_server', 'import_template'
+const workflows = {
+ 'btn-new-project': 'new_project',
+ 'btn-connect-server': 'connect_server',
+ 'btn-import-template': 'import_template',
+};
+
+function updateVisibility() {
+ // 1. Handle entry points (cross-contamination prevention)
+ Object.keys(workflows).forEach((id) => {
+ const el = document.getElementById(id);
+ if (!el) return;
+
+ // If no workflow is active, show all entry points
+ if (!currentWorkflow) {
+ el.classList.remove('hidden-action');
+ }
+ // If a workflow is active, ONLY show the chosen entry point
+ else if (currentWorkflow === workflows[id]) {
+ el.classList.remove('hidden-action');
+ }
+ // Hide the other entry points
+ else {
+ el.classList.add('hidden-action');
+ }
+ });
+
+ // 2. Helper to toggle visibility and cascade reset if dependencies fail
+ const toggle = (id, condition) => {
+ const el = document.getElementById(id);
+ if (!el) return;
+
+ if (condition) {
+ if (el.classList.contains('hidden-action')) {
+ el.classList.remove('hidden-action');
+ el.classList.add('revealed');
+ }
+ } else {
+ el.classList.add('hidden-action');
+ el.classList.remove('revealed');
+
+ // Cascade reset
+ if (state[buttonToStateMap[id]]) {
+ state[buttonToStateMap[id]] = false;
+ el.classList.remove('completed');
+ }
+ }
+ };
+
+ // --- Dynamic Path Routing ---
+
+ // Branch A: New Project
+ toggle('btn-init-db', currentWorkflow === 'new_project' && state.newProject);
+
+ // Branch B: Connect
+ toggle('btn-enter-credentials', currentWorkflow === 'connect_server' && state.connectServer);
+
+ // Branch C: Import
+ toggle('btn-select-template', currentWorkflow === 'import_template' && state.importTemplate);
+ toggle('btn-customize-template', state.templateSelected);
+
+ // Shared Path Entry (Branches A & B merge here)
+ const hasSchemaSource = state.dbInitialized || state.credentialsEntered;
+ toggle('btn-fetch-schema', hasSchemaSource);
+
+ toggle('btn-clean-data', state.schemaFetched);
+ toggle('btn-join-tables', state.schemaFetched);
+
+ const hasCleanData = state.dataCleaned && state.tablesJoined;
+ toggle('btn-apply-filter', hasCleanData);
+ toggle('btn-analyze-trends', state.filterApplied);
+
+ // Report Generation (Branches A, B, and C merge here)
+ const canGenerateReport = state.trendsAnalyzed || state.templateCustomized;
+ toggle('btn-generate-report', canGenerateReport);
+
+ toggle('btn-export-pdf', state.reportGenerated);
+ toggle('btn-send-email', state.reportGenerated);
+}
+
+// Handle manual clicks to toggle completion and trigger visibility updates
+Object.keys(buttonToStateMap).forEach((id) => {
+ const btn = document.getElementById(id);
+ if (btn) {
+ btn.addEventListener('click', () => {
+ // Manage Workflow Selection
+ if (workflows[id]) {
+ currentWorkflow = workflows[id];
+ }
+
+ // Toggle state
+ const stateKey = buttonToStateMap[id];
+ state[stateKey] = !state[stateKey];
+
+ // Update UI
+ if (state[stateKey]) {
+ btn.classList.add('completed');
+ } else {
+ btn.classList.remove('completed');
+ // Un-selecting a top-level workflow resets the board
+ if (workflows[id]) {
+ currentWorkflow = null;
+ }
+ }
+
+ // Update visibility of dependent buttons
+ updateVisibility();
+ });
+ }
+});
+
+// Reset Button Logic
+const resetBtn = document.getElementById('btn-reset');
+if (resetBtn) {
+ resetBtn.addEventListener('click', () => {
+ // Reset all state variables
+ Object.keys(state).forEach((key) => { state[key] = false; });
+ currentWorkflow = null;
+
+ // Remove completed styling from all buttons
+ document.querySelectorAll('.action-btn').forEach((btn) => {
+ btn.classList.remove('completed');
+ });
+
+ updateVisibility();
+ });
+}
+
+// Suggestion Chips Logic
+document.querySelectorAll('.suggestion-chip').forEach(chip => {
+ chip.addEventListener('click', (e) => {
+ e.preventDefault();
+ const goalInput = document.getElementById('goal-input');
+ if (goalInput) {
+ goalInput.value = chip.textContent.trim();
+ // Dispatch events in case other scripts monitor this
+ goalInput.dispatchEvent(new Event('input', { bubbles: true }));
+ goalInput.dispatchEvent(new Event('change', { bubbles: true }));
+
+ // Visual feedback
+ goalInput.style.backgroundColor = '#f0fdf4';
+ goalInput.style.transition = 'background-color 0.3s ease';
+
+ // Flash the text color too for extra visibility
+ goalInput.style.color = '#15803d';
+
+ setTimeout(() => {
+ goalInput.style.backgroundColor = '';
+ goalInput.style.color = '';
+ }, 500);
+ }
+ });
+});
+
+// Initialize UI state on load
+updateVisibility();
+
+// Handle external tool cancel events
+window.addEventListener('toolcancel', () => {
+ setAgentThinking(false);
+});
+
+// Helper for highlighting an element
+function showHighlight(elementId, instruction) {
+ const el = document.getElementById(elementId);
+ const backdrop = document.getElementById('backdrop');
+ const tooltip = document.getElementById('tooltip');
+
+ // Verify the element exists and is visible
+ if (!el || el.classList.contains('hidden-action') || !backdrop || !tooltip) {
+ return false;
+ }
+
+ backdrop.classList.remove('hidden');
+ el.classList.add('highlighted');
+
+ tooltip.innerText = instruction;
+ const rect = el.getBoundingClientRect();
+ tooltip.style.left = `${rect.left + window.scrollX}px`;
+ tooltip.style.top = `${rect.bottom + 15 + window.scrollY}px`;
+ tooltip.classList.remove('hidden');
+
+ el.scrollIntoView({ behavior: 'smooth', block: 'center' });
+
+ return true;
+}
+
+function hideHighlight(elementId) {
+ const el = document.getElementById(elementId);
+ const backdrop = document.getElementById('backdrop');
+ const tooltip = document.getElementById('tooltip');
+
+ if (el) el.classList.remove('highlighted');
+ if (backdrop) backdrop.classList.add('hidden');
+ if (tooltip) tooltip.classList.add('hidden');
+}
+
+// --- Agent UI State Management ---
+function setAgentThinking(isThinking, message = 'WebMCP Agent is thinking...') {
+ const statusEl = document.getElementById('agent-status');
+ const textEl = document.querySelector('.status-text');
+ if (statusEl && textEl) {
+ if (isThinking) {
+ statusEl.classList.remove('hidden');
+ textEl.innerText = message;
+ } else {
+ statusEl.classList.add('hidden');
+ }
+ }
+}
+
+// Helper to get visible buttons for the Agent payload
+function getVisibleButtons() {
+ const buttons = [];
+ Object.keys(buttonToStateMap).forEach((id) => {
+ const el = document.getElementById(id);
+ if (el && !el.classList.contains('hidden-action')) {
+ buttons.push({
+ id,
+ label: el.innerText.trim().replace(/\n.*$/, ''),
+ completed: state[buttonToStateMap[id]]
+ });
+ }
+ });
+ return buttons;
+}
+
+// --- WebMCP Tool Registration ---
+if (window.navigator.modelContext) {
+ document.body.classList.add('webmcp-supported');
+
+ navigator.modelContext.registerTool({
+ name: 'survey_ui_state',
+ description: 'Get a list of currently VISIBLE and interactable dashboard buttons, along with the user\'s ultimate goal. Call this to map user intent to the correct entry path, or after any interaction to discover newly revealed paths.',
+ inputSchema: {
+ type: 'object',
+ properties: {},
+ },
+ execute: () => {
+ setAgentThinking(true, 'Agent is surveying the UI...');
+ const goalText = document.getElementById('goal-input').value.trim();
+
+ return {
+ ultimate_goal: goalText || 'No specific goal entered yet.',
+ active_workflow: currentWorkflow || 'none_selected',
+ visible_buttons: getVisibleButtons(),
+ current_state: state
+ };
+ },
+ });
+
+ navigator.modelContext.registerTool({
+ name: 'highlight_ui_element',
+ description: 'Highlight a specific visible dashboard button to guide the user. Execution pauses until the user clicks the highlighted button.',
+ inputSchema: {
+ type: 'object',
+ properties: {
+ element_id: {
+ type: 'string',
+ description: 'The ID of the HTML button to highlight. MUST be one of the button IDs returned by survey_ui_state.'
+ },
+ instruction: {
+ type: 'string',
+ description: 'A dynamically generated short instruction explaining to the user exactly why clicking this button helps achieve their unique goal.'
+ }
+ },
+ required: ['element_id', 'instruction'],
+ },
+ execute: ({ element_id, instruction }) => {
+ return new Promise((resolve, reject) => {
+ // Agent is done thinking, waiting for user
+ setAgentThinking(false);
+
+ const success = showHighlight(element_id, instruction);
+ if (!success) {
+ return resolve(`Failed to highlight: Element with id '${element_id}' not found or is currently hidden/locked.`);
+ }
+
+ const el = document.getElementById(element_id);
+
+ const onClick = () => {
+ hideHighlight(element_id);
+ el.removeEventListener('click', onClick);
+
+ // User clicked, hand control back to Agent, show thinking UI again
+ setAgentThinking(true, 'Agent is processing the next step...');
+
+ // Force a microtask delay to ensure UI updates before reading visible buttons
+ setTimeout(() => {
+ const goalText = document.getElementById('goal-input').value.trim();
+ const visibleButtonsList = getVisibleButtons().map(b => `'${b.id}' (${b.label})`).join(', ');
+
+ // Contextual Handshaking string returned directly to the Agent
+ resolve(`User clicked ${element_id}. Here is the new list of visible buttons: ${visibleButtonsList}. What is the next step to reach the goal: "${goalText}"?`);
+ }, 0);
+ };
+
+ el.addEventListener('click', onClick);
+ });
+ },
+ });
+
+ navigator.modelContext.registerTool({
+ name: 'finish_guidance',
+ description: 'Call this tool when the user\'s ultimate goal has been successfully achieved to end the guidance session and update the UI.',
+ inputSchema: {
+ type: 'object',
+ properties: {
+ message: {
+ type: 'string',
+ description: 'A congratulatory message to the user.'
+ }
+ },
+ required: ['message'],
+ },
+ execute: ({ message }) => {
+ setAgentThinking(false);
+ alert(`Guidance Complete: ${message}`);
+ return 'Guidance session ended successfully.';
+ },
+ });
+}
diff --git a/demos/guided-ui/style.css b/demos/guided-ui/style.css
new file mode 100644
index 00000000..12032326
--- /dev/null
+++ b/demos/guided-ui/style.css
@@ -0,0 +1,344 @@
+/**
+ * Copyright 2026 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+:root {
+ --primary-color: #2563eb;
+ --bg-color: #f8fafc;
+ --surface-color: #ffffff;
+ --text-main: #0f172a;
+ --text-muted: #64748b;
+ --border-color: #e2e8f0;
+ --highlight-shadow: rgba(37, 99, 235, 0.4);
+ --success-color: #10b981;
+}
+
+body {
+ margin: 0;
+ padding: 0;
+ font-family: 'Inter', sans-serif;
+ background-color: var(--bg-color);
+ color: var(--text-main);
+ line-height: 1.5;
+}
+
+.app-header {
+ background-color: var(--surface-color);
+ padding: 24px 32px;
+ border-bottom: 1px solid var(--border-color);
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
+}
+
+.app-header h1 {
+ margin: 0;
+ font-size: 24px;
+ font-weight: 600;
+}
+
+.app-header p {
+ margin: 4px 0 0;
+ color: var(--text-muted);
+ font-size: 14px;
+}
+
+.warning-banner {
+ background-color: #fef2f2;
+ border-left: 4px solid #ef4444;
+ color: #b91c1c;
+ padding: 16px;
+ border-radius: 4px;
+ font-size: 14px;
+ margin-bottom: -16px;
+}
+
+body.webmcp-supported .warning-banner {
+ display: none;
+}
+
+.dashboard {
+ max-width: 1000px;
+ margin: 40px auto;
+ padding: 0 20px;
+ display: grid;
+ gap: 32px;
+}
+
+.goal-section {
+ background: var(--surface-color);
+ padding: 24px;
+ border-radius: 12px;
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
+}
+
+.goal-section h2 {
+ margin-top: 0;
+ font-size: 18px;
+}
+
+#goal-input {
+ width: 100%;
+ padding: 12px;
+ border: 1px solid var(--border-color);
+ border-radius: 8px;
+ font-family: inherit;
+ font-size: 16px;
+ resize: vertical;
+ box-sizing: border-box;
+}
+
+#goal-input:focus {
+ outline: none;
+ border-color: var(--primary-color);
+ box-shadow: 0 0 0 3px var(--highlight-shadow);
+}
+
+.instruction-text {
+ font-size: 14px;
+ color: var(--text-muted);
+ margin-bottom: 0;
+ margin-top: 12px;
+}
+
+.suggestions-container {
+ margin-top: 12px;
+ display: flex;
+ flex-wrap: wrap;
+ gap: 8px;
+ align-items: center;
+}
+
+.suggestions-label {
+ font-size: 13px;
+ color: var(--text-muted);
+ font-weight: 500;
+ margin-right: 4px;
+}
+
+.suggestion-chip {
+ background: var(--bg-color);
+ border: 1px solid var(--border-color);
+ padding: 6px 12px;
+ border-radius: 16px;
+ font-size: 13px;
+ color: var(--text-main);
+ cursor: pointer;
+ transition: all 0.2s;
+ text-align: left;
+}
+
+.suggestion-chip:hover {
+ background: #e2e8f0;
+ border-color: #cbd5e1;
+}
+
+.agent-status {
+ margin-top: 16px;
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ background: #eff6ff;
+ border: 1px solid #bfdbfe;
+ color: #1d4ed8;
+ padding: 12px 16px;
+ border-radius: 8px;
+ font-size: 14px;
+ font-weight: 500;
+ animation: pulse 2s infinite ease-in-out;
+}
+
+.agent-status.hidden {
+ display: none;
+}
+
+.agent-status .spinner {
+ display: inline-block;
+ animation: spin 2s linear infinite;
+}
+
+@keyframes spin {
+ 100% { transform: rotate(360deg); }
+}
+
+@keyframes pulse {
+ 0%, 100% { opacity: 1; }
+ 50% { opacity: 0.7; }
+}
+
+.actions-section {
+ background: var(--surface-color);
+ padding: 32px;
+ border-radius: 12px;
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
+}
+
+.actions-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: 24px;
+}
+
+.actions-header h2 {
+ margin: 0;
+ font-size: 18px;
+}
+
+.secondary-btn {
+ background: var(--surface-color);
+ border: 1px solid var(--border-color);
+ padding: 8px 16px;
+ border-radius: 6px;
+ font-family: inherit;
+ font-size: 14px;
+ font-weight: 500;
+ color: var(--text-muted);
+ cursor: pointer;
+ transition: all 0.2s;
+}
+
+.secondary-btn:hover {
+ background: #f1f5f9;
+ color: var(--text-main);
+}
+
+.button-grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
+ gap: 16px;
+}
+
+.action-btn {
+ background: var(--surface-color);
+ border: 1px solid var(--border-color);
+ padding: 16px;
+ border-radius: 8px;
+ font-family: inherit;
+ font-size: 14px;
+ font-weight: 500;
+ color: var(--text-main);
+ cursor: pointer;
+ display: flex;
+ align-items: center;
+ justify-content: flex-start;
+ gap: 12px;
+ transition: all 0.2s ease-in-out;
+ position: relative;
+ text-align: left;
+}
+
+.action-btn:hover {
+ background-color: var(--bg-color);
+ border-color: #cbd5e1;
+}
+
+.action-btn .icon {
+ font-size: 18px;
+}
+
+.status-indicator {
+ position: absolute;
+ right: 16px;
+ width: 16px;
+ height: 16px;
+ border-radius: 50%;
+ border: 2px solid #cbd5e1;
+ transition: all 0.3s ease;
+}
+
+.action-btn.completed {
+ border-color: var(--success-color);
+ background-color: #f0fdf4;
+}
+
+.action-btn.completed .status-indicator {
+ border-color: var(--success-color);
+ background-color: var(--success-color);
+}
+
+.action-btn.completed .status-indicator::after {
+ content: 'โ';
+ color: white;
+ position: absolute;
+ top: 50%;
+ left: 50%;
+ transform: translate(-50%, -50%);
+ font-size: 10px;
+ font-weight: bold;
+}
+
+/* Visibility states */
+.action-btn.hidden-action {
+ display: none !important;
+}
+
+@keyframes fadeIn {
+ from { opacity: 0; transform: translateY(10px); }
+ to { opacity: 1; transform: translateY(0); }
+}
+
+.action-btn.revealed {
+ animation: fadeIn 0.4s ease-out forwards;
+}
+
+/* Guidance Highlight State */
+#backdrop {
+ position: fixed;
+ top: 0;
+ left: 0;
+ width: 100vw;
+ height: 100vh;
+ background: rgba(0, 0, 0, 0.5);
+ backdrop-filter: blur(2px);
+ z-index: 100;
+ transition: opacity 0.3s ease;
+}
+
+#backdrop.hidden {
+ opacity: 0;
+ pointer-events: none;
+}
+
+/* The button being highlighted */
+.action-btn.highlighted {
+ z-index: 101;
+ position: relative;
+ border-color: var(--primary-color);
+ box-shadow: 0 0 0 4px var(--highlight-shadow), 0 10px 15px -3px rgba(0, 0, 0, 0.1);
+ background-color: var(--surface-color);
+ transform: scale(1.05);
+}
+
+/* Tooltip container */
+#tooltip {
+ position: absolute;
+ z-index: 102;
+ background: var(--primary-color);
+ color: white;
+ padding: 12px 16px;
+ border-radius: 8px;
+ font-size: 14px;
+ font-weight: 500;
+ box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
+ max-width: 250px;
+ line-height: 1.4;
+ transition: opacity 0.3s ease, transform 0.3s ease;
+ pointer-events: none;
+}
+
+#tooltip::before {
+ content: '';
+ position: absolute;
+ top: -6px;
+ left: 20px;
+ width: 0;
+ height: 0;
+ border-left: 6px solid transparent;
+ border-right: 6px solid transparent;
+ border-bottom: 6px solid var(--primary-color);
+}
+
+#tooltip.hidden {
+ opacity: 0;
+ transform: translateY(10px);
+}