/* ========================================================================== Craft.js Site Builder — Dark Theme Base: #16161a | Accent: #3b82f6 | Text: #e4e4e7 | Border: #2d2d3a ========================================================================== */ /* -------------------------------------------------------------------------- CSS Variables -------------------------------------------------------------------------- */ :root { --color-bg-base: #16161a; --color-bg-surface: #1c1c24; --color-bg-elevated: #22222c; --color-bg-canvas: #0f0f14; --color-bg-hover: #3f3f46; --color-bg-active: #2d2d3a; --color-border: #2d2d3a; --color-border-light: #3f3f46; --color-text: #e4e4e7; --color-text-muted: #8b8b96; --color-text-dim: #6e6e78; --color-accent: #3b82f6; --color-accent-hover: #2563eb; --color-accent-subtle: rgba(59, 130, 246, 0.12); --color-danger: #ef4444; --color-success: #10b981; --topbar-height: 50px; --panel-left-width: 260px; --panel-right-width: 320px; --radius-sm: 4px; --radius-md: 6px; --radius-lg: 8px; --transition-fast: 0.15s ease; --transition-normal: 0.25s ease; /* Phase A (mobile): fixed bottom tab bar height, used to size .editor-container and the safe-area padding math below. */ --mobile-tabbar-height: 58px; /* Phase B fast-follow item 4: rendered height of `.mobile-selection-toolbar` (44px min-height content + 4px top/bottom padding + 1px border) -- used to pad the canvas scroll area clear of it while it's visible, see `.editor-canvas.has-mobile-selection-toolbar` below. */ --mobile-selection-toolbar-height: 53px; /* Mobile-A2: explicit overlay layer scale (was ad-hoc numbers scattered across the mobile block + Modal.tsx, causing accidental stacking -- e.g. the Templates modal getting trapped under the tab bar inside .topbar's own stacking context). Higher = closer to the user. --z-selection-toolbar is reserved for Phase B's selection toolbar. */ --z-sheet-backdrop: 400; --z-tabbar: 500; --z-selection-toolbar: 600; --z-modal: 5000; --z-overflow: 10000; } /* -------------------------------------------------------------------------- Modals (shared by Modal.tsx -- TemplateModal, HeadCodeModal, SitesmithModal) -------------------------------------------------------------------------- */ .modal-backdrop { z-index: var(--z-modal); } /* -------------------------------------------------------------------------- Reset & Base -------------------------------------------------------------------------- */ *, *::before, *::after { box-sizing: border-box; } body { margin: 0; padding: 0; font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; font-size: 13px; color: var(--color-text); background: var(--color-bg-base); -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; overflow: hidden; } /* -------------------------------------------------------------------------- Editor App — Full Viewport -------------------------------------------------------------------------- */ .editor-app { display: flex; flex-direction: column; width: 100vw; height: 100vh; overflow: hidden; background: var(--color-bg-base); } /* -------------------------------------------------------------------------- Top Navigation Bar -------------------------------------------------------------------------- */ .topbar { display: flex; align-items: center; justify-content: space-between; height: var(--topbar-height); min-height: var(--topbar-height); padding: 0 12px; background: var(--color-bg-surface); border-bottom: 1px solid var(--color-border); z-index: 100; gap: 12px; } .topbar-left, .topbar-center, .topbar-right { display: flex; align-items: center; gap: 8px; } .topbar-left { flex: 1; min-width: 0; } .topbar-center { flex: 0 0 auto; } .topbar-right { flex: 1; min-width: 0; justify-content: flex-end; } .topbar-title { font-weight: 600; font-size: 14px; color: var(--color-text); white-space: nowrap; } .topbar-domain { display: inline-block; padding: 2px 8px; font-size: 11px; color: var(--color-text-muted); background: var(--color-bg-active); border-radius: 10px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 180px; } /* Top Bar Buttons */ .topbar-btn { display: inline-flex; align-items: center; gap: 6px; padding: 6px 12px; font-size: 12px; font-weight: 500; color: var(--color-text); background: transparent; border: 1px solid var(--color-border); border-radius: var(--radius-md); cursor: pointer; white-space: nowrap; transition: background var(--transition-fast), border-color var(--transition-fast), color var(--transition-fast); text-decoration: none; line-height: 1.4; } .topbar-btn:hover { background: var(--color-bg-hover); border-color: var(--color-border-light); } .topbar-btn:active { background: var(--color-bg-active); } .topbar-btn:disabled { opacity: 0.35; cursor: not-allowed; } .topbar-btn:disabled:hover { background: transparent; border-color: var(--color-border); } .topbar-btn.primary { background: var(--color-accent); border-color: var(--color-accent); color: #ffffff; } .topbar-btn.primary:hover { background: var(--color-accent-hover); border-color: var(--color-accent-hover); } .topbar-btn.primary:active { background: #1d4ed8; } .topbar-btn.back-btn { border-color: transparent; color: var(--color-text-muted); } .topbar-btn.back-btn:hover { color: var(--color-text); background: var(--color-bg-hover); } /* Top Bar Divider */ .topbar-divider { display: inline-block; width: 1px; height: 24px; background: var(--color-border); margin: 0 4px; } /* Icon-only topbar buttons (Show Guides, Code, Preview) -- square-ish footprint instead of the label+icon pill, tooltip via [data-tooltip]. */ .topbar-btn.icon-only { gap: 0; padding: 6px 8px; } .topbar-btn.guides-toggle.active { color: var(--color-accent); background: var(--color-accent-subtle); border-color: var(--color-accent); } /* -------------------------------------------------------------------------- Device Switcher -------------------------------------------------------------------------- */ .device-switcher { display: flex; background: var(--color-bg-base); border: 1px solid var(--color-border); border-radius: var(--radius-md); overflow: hidden; } .device-btn { display: inline-flex; align-items: center; justify-content: center; width: 36px; height: 32px; font-size: 14px; color: var(--color-text-muted); background: transparent; border: none; cursor: pointer; transition: background var(--transition-fast), color var(--transition-fast); } .device-btn:hover { color: var(--color-text); background: var(--color-bg-hover); } .device-btn.active { color: var(--color-accent); background: var(--color-accent-subtle); } .device-btn + .device-btn { border-left: 1px solid var(--color-border); } /* -------------------------------------------------------------------------- Editor Container (3-column layout) -------------------------------------------------------------------------- */ .editor-container { display: flex; flex: 1; height: calc(100vh - var(--topbar-height)); overflow: hidden; } /* -------------------------------------------------------------------------- Left Panel -------------------------------------------------------------------------- */ .panel-left { width: var(--panel-left-width); min-width: var(--panel-left-width); display: flex; flex-direction: column; background: var(--color-bg-surface); border-right: 1px solid var(--color-border); overflow: hidden; } /* -------------------------------------------------------------------------- Right Panel -------------------------------------------------------------------------- */ .panel-right { width: var(--panel-right-width); min-width: var(--panel-right-width); display: flex; flex-direction: column; background: var(--color-bg-surface); border-left: 1px solid var(--color-border); overflow: hidden; } /* -------------------------------------------------------------------------- Panel Tabs -------------------------------------------------------------------------- */ .panel-tabs { display: flex; border-bottom: 1px solid var(--color-border); background: var(--color-bg-surface); flex-shrink: 0; } .panel-tab { flex: 1; padding: 10px 0; font-size: 11px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.5px; color: var(--color-text-muted); background: transparent; border: none; border-bottom: 2px solid transparent; cursor: pointer; transition: color var(--transition-fast), border-color var(--transition-fast); } .panel-tab:hover { color: var(--color-text); } .panel-tab.active { color: var(--color-accent); border-bottom-color: var(--color-accent); } /* -------------------------------------------------------------------------- Panel Content -------------------------------------------------------------------------- */ .panel-content { flex: 1; overflow-y: auto; padding: 12px; } /* -------------------------------------------------------------------------- Panel Placeholder -------------------------------------------------------------------------- */ .panel-placeholder { display: flex; align-items: center; justify-content: center; height: 120px; color: var(--color-text-dim); font-size: 12px; font-style: italic; } /* -------------------------------------------------------------------------- Canvas Area -------------------------------------------------------------------------- */ .editor-canvas { flex: 1; overflow: auto; background: var(--color-bg-canvas); padding: 24px; position: relative; } .canvas-device-frame { background: #ffffff; box-shadow: 0 0 0 1px rgba(255, 255, 255, 0.06), 0 4px 24px rgba(0, 0, 0, 0.4); border-radius: 2px; min-height: 100%; } /* -------------------------------------------------------------------------- No Selection Message -------------------------------------------------------------------------- */ .no-selection { display: flex; flex-direction: column; align-items: center; justify-content: center; text-align: center; padding: 40px 20px; color: var(--color-text-dim); font-size: 12px; line-height: 1.6; } .no-selection i { display: block; font-size: 28px; margin-bottom: 12px; opacity: 0.25; color: var(--color-text-muted); } .no-selection p { margin: 0; max-width: 180px; } /* -------------------------------------------------------------------------- Guided Styles -------------------------------------------------------------------------- */ .guided-styles { padding: 4px 0; } .guided-section { margin-bottom: 20px; } .guided-section:last-child { margin-bottom: 0; } .guided-section-label { display: block; font-size: 11px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.5px; color: var(--color-text-muted); margin-bottom: 8px; } .guided-type-header { margin-bottom: 16px; padding-bottom: 12px; border-bottom: 1px solid var(--color-border); } .guided-type-badge { display: inline-flex; align-items: center; gap: 6px; font-size: 12px; font-weight: 600; color: var(--color-text); background: var(--color-bg-elevated); padding: 4px 10px; border-radius: var(--radius-sm); border: 1px solid var(--color-border); } .guided-type-badge i { color: var(--color-accent); font-size: 11px; } /* Preset grid: 4 columns for swatches/buttons */ .preset-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 6px; } .preset-grid.align-grid { grid-template-columns: repeat(4, 1fr); } .preset-grid.gradient-grid { grid-template-columns: repeat(4, 1fr); } /* Circular/square color swatch */ .preset-swatch { width: 100%; aspect-ratio: 1; border-radius: var(--radius-sm); border: 2px solid var(--color-border); cursor: pointer; transition: border-color var(--transition-fast), box-shadow var(--transition-fast); padding: 0; font-size: 0; line-height: 1; } .preset-swatch:hover { border-color: #a1a1aa; } .preset-swatch.active { border-color: var(--color-accent); box-shadow: 0 0 0 2px var(--color-accent); } .preset-swatch.gradient-swatch { border-radius: var(--radius-sm); font-size: 12px; color: #a1a1aa; display: flex; align-items: center; justify-content: center; } /* Text preset button */ .preset-btn { padding: 6px 4px; font-size: 11px; font-weight: 500; border-radius: var(--radius-sm); border: 1px solid var(--color-border); background: var(--color-bg-elevated); color: var(--color-text-muted); cursor: pointer; transition: border-color var(--transition-fast), background var(--transition-fast), color var(--transition-fast); text-align: center; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .preset-btn:hover { border-color: #a1a1aa; color: var(--color-text); background: var(--color-bg-hover); } .preset-btn.active { border-color: var(--color-accent); background: var(--color-accent); color: #ffffff; } .preset-btn.apply-btn { flex-shrink: 0; padding: 6px 12px; background: var(--color-accent); color: #ffffff; border-color: var(--color-accent); } .preset-btn.apply-btn:hover { background: #2563eb; } /* Guided input field */ .guided-input { width: 100%; padding: 6px 8px; background: var(--color-bg-elevated); color: var(--color-text); border: 1px solid var(--color-border); border-radius: var(--radius-sm); font-size: 12px; outline: none; transition: border-color var(--transition-fast); box-sizing: border-box; } .guided-input:focus { border-color: var(--color-accent); } .guided-input::placeholder { color: var(--color-text-dim); } /* Input row with Apply button */ .guided-input-row { display: flex; gap: 6px; } .guided-input-row .guided-input { flex: 1; min-width: 0; } /* Save status indicator in topbar */ .save-indicator { display: inline-flex; align-items: center; gap: 4px; font-size: 11px; font-weight: 500; padding: 4px 8px; border-radius: var(--radius-sm); animation: fadeIn 0.2s ease; } .save-indicator.saved { color: #10b981; } .save-indicator.error { color: #ef4444; } /* Publish button */ .topbar-btn.publish { background: #10b981; border-color: #10b981; color: #ffffff; } .topbar-btn.publish:hover { background: #059669; border-color: #059669; } .topbar-btn.publish:active { background: #047857; } .topbar-btn.publish:disabled { background: #6b7280; border-color: #6b7280; cursor: not-allowed; opacity: 0.7; } /* Draft/Published badge */ .publish-badge { display: inline-flex; align-items: center; gap: 4px; font-size: 11px; padding: 2px 8px; border-radius: 10px; font-weight: 600; letter-spacing: 0.3px; text-transform: uppercase; animation: fadeIn 0.2s ease; } .publish-badge.draft { background: rgba(245, 158, 11, 0.15); color: #f59e0b; border: 1px solid rgba(245, 158, 11, 0.3); } .publish-badge.published { background: rgba(16, 185, 129, 0.15); color: #10b981; border: 1px solid rgba(16, 185, 129, 0.3); } @keyframes fadeIn { from { opacity: 0; transform: translateY(-2px); } to { opacity: 1; transform: translateY(0); } } /* -------------------------------------------------------------------------- Block Categories -------------------------------------------------------------------------- */ .block-category { margin-bottom: 4px; } .block-category-header { display: flex; align-items: center; justify-content: space-between; padding: 8px 4px; font-size: 11px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.5px; color: var(--color-text-muted); cursor: pointer; border-radius: var(--radius-sm); transition: background var(--transition-fast); user-select: none; } .block-category-header:hover { background: var(--color-bg-hover); color: var(--color-text); } .block-category-header .chevron { font-size: 10px; transition: transform var(--transition-fast); } .block-category-header.collapsed .chevron { transform: rotate(-90deg); } .block-category-items { overflow: hidden; } .block-category-items.collapsed { display: none; } /* -------------------------------------------------------------------------- Block Grid -------------------------------------------------------------------------- */ .block-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 6px; padding: 4px 0 12px; } /* -------------------------------------------------------------------------- Block Item (draggable card) -------------------------------------------------------------------------- */ .block-item { display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 6px; padding: 12px 8px; background: var(--color-bg-elevated); border: 1px solid var(--color-border); border-radius: var(--radius-md); cursor: grab; transition: border-color var(--transition-fast), background var(--transition-fast), box-shadow var(--transition-fast); user-select: none; } .block-item:hover { border-color: var(--color-accent); background: var(--color-accent-subtle); box-shadow: 0 0 0 1px var(--color-accent); } .block-item:active { cursor: grabbing; opacity: 0.8; } .block-item-icon { font-size: 20px; color: var(--color-text-muted); transition: color var(--transition-fast); } .block-item:hover .block-item-icon { color: var(--color-accent); } .block-item-label { font-size: 11px; font-weight: 500; color: var(--color-text-muted); text-align: center; line-height: 1.2; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 100%; } .block-item:hover .block-item-label { color: var(--color-text); } /* -------------------------------------------------------------------------- Color Preset Buttons -------------------------------------------------------------------------- */ .color-presets { display: flex; flex-wrap: wrap; gap: 6px; } .color-preset { width: 28px; height: 28px; border-radius: 50%; border: 2px solid var(--color-border); cursor: pointer; transition: border-color var(--transition-fast), transform var(--transition-fast), box-shadow var(--transition-fast); position: relative; } .color-preset:hover { border-color: var(--color-text-muted); transform: scale(1.1); } .color-preset.active { border-color: var(--color-accent); box-shadow: 0 0 0 2px var(--color-accent-subtle); } .color-preset::after { content: ''; position: absolute; inset: 2px; border-radius: 50%; background: inherit; } /* -------------------------------------------------------------------------- Size Preset Buttons -------------------------------------------------------------------------- */ .size-presets { display: flex; gap: 4px; } .size-preset { display: inline-flex; align-items: center; justify-content: center; min-width: 36px; height: 30px; padding: 0 8px; font-size: 11px; font-weight: 600; color: var(--color-text-muted); background: var(--color-bg-elevated); border: 1px solid var(--color-border); border-radius: var(--radius-sm); cursor: pointer; transition: all var(--transition-fast); } .size-preset:hover { color: var(--color-text); border-color: var(--color-border-light); background: var(--color-bg-hover); } .size-preset.active { color: var(--color-accent); border-color: var(--color-accent); background: var(--color-accent-subtle); } /* -------------------------------------------------------------------------- Gradient Presets -------------------------------------------------------------------------- */ .gradient-presets { display: grid; grid-template-columns: repeat(4, 1fr); gap: 6px; } .gradient-preset { width: 100%; aspect-ratio: 1.6; border-radius: var(--radius-sm); border: 2px solid var(--color-border); cursor: pointer; transition: border-color var(--transition-fast), transform var(--transition-fast); } .gradient-preset:hover { border-color: var(--color-text-muted); transform: scale(1.05); } .gradient-preset.active { border-color: var(--color-accent); } /* -------------------------------------------------------------------------- Font Family Presets -------------------------------------------------------------------------- */ .font-presets { display: flex; flex-direction: column; gap: 2px; } .font-preset { display: flex; align-items: center; padding: 6px 8px; font-size: 12px; color: var(--color-text-muted); background: transparent; border: 1px solid transparent; border-radius: var(--radius-sm); cursor: pointer; transition: all var(--transition-fast); } .font-preset:hover { color: var(--color-text); background: var(--color-bg-hover); } .font-preset.active { color: var(--color-accent); background: var(--color-accent-subtle); border-color: var(--color-accent); } /* -------------------------------------------------------------------------- Form Controls -------------------------------------------------------------------------- */ .control-group { margin-bottom: 16px; } .control-label { display: block; font-size: 11px; font-weight: 600; color: var(--color-text-muted); margin-bottom: 6px; text-transform: uppercase; letter-spacing: 0.3px; } .control-input { width: 100%; padding: 7px 10px; font-size: 12px; color: var(--color-text); background: var(--color-bg-base); border: 1px solid var(--color-border); border-radius: var(--radius-sm); outline: none; transition: border-color var(--transition-fast); } .control-input:focus { border-color: var(--color-accent); } .control-input::placeholder { color: var(--color-text-dim); } .control-select { width: 100%; padding: 7px 10px; font-size: 12px; color: var(--color-text); background: var(--color-bg-base); border: 1px solid var(--color-border); border-radius: var(--radius-sm); outline: none; cursor: pointer; appearance: none; background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath fill='%2371717a' d='M3 5l3 3 3-3'/%3E%3C/svg%3E"); background-repeat: no-repeat; background-position: right 8px center; padding-right: 28px; } .control-select:focus { border-color: var(--color-accent); } /* -------------------------------------------------------------------------- Slider Control -------------------------------------------------------------------------- */ .control-slider { width: 100%; height: 4px; appearance: none; background: var(--color-bg-active); border-radius: 2px; outline: none; cursor: pointer; } .control-slider::-webkit-slider-thumb { appearance: none; width: 14px; height: 14px; border-radius: 50%; background: var(--color-accent); border: 2px solid var(--color-bg-surface); cursor: pointer; box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3); } .control-slider::-moz-range-thumb { width: 14px; height: 14px; border-radius: 50%; background: var(--color-accent); border: 2px solid var(--color-bg-surface); cursor: pointer; box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3); } /* -------------------------------------------------------------------------- Toggle Switch -------------------------------------------------------------------------- */ .toggle-switch { position: relative; display: inline-block; width: 36px; height: 20px; } .toggle-switch input { opacity: 0; width: 0; height: 0; } .toggle-track { position: absolute; inset: 0; background: var(--color-bg-active); border-radius: 10px; cursor: pointer; transition: background var(--transition-fast); } .toggle-track::after { content: ''; position: absolute; top: 2px; left: 2px; width: 16px; height: 16px; background: #ffffff; border-radius: 50%; transition: transform var(--transition-fast); } .toggle-switch input:checked + .toggle-track { background: var(--color-accent); } .toggle-switch input:checked + .toggle-track::after { transform: translateX(16px); } /* -------------------------------------------------------------------------- Editor Visual Guides — dashed outlines for real drop targets only -------------------------------------------------------------------------- */ /* Only actual Craft.js droppable containers get a guide outline -- these are tagged with `data-craft-node` in RenderNode.tsx (the `` override) based on `node.data.isCanvas`, not a blanket tag selector. A component's own internal wrapper `
`s (e.g. Section's inner max-width container) are real DOM elements but not Craft nodes, so they never get this attribute and never get a guide outline -- this is what stops finished sections from showing 3-4 nested dashed boxes. */ .canvas-device-frame [data-craft-node] { outline: 1px dashed rgba(148, 163, 184, 0.2); outline-offset: -1px; transition: outline-color 0.15s; } /* Rows (flex containers) get a slightly more visible border */ .canvas-device-frame [data-craft-node][style*="display: flex"], .canvas-device-frame [data-craft-node][style*="display:flex"] { outline: 1px dashed rgba(148, 163, 184, 0.35); } /* Empty containers: more visible + minimum height so they're clickable */ .canvas-device-frame [data-craft-node]:empty { outline: 2px dashed rgba(148, 163, 184, 0.4); min-height: 60px; } /* Hover highlights */ .canvas-device-frame [data-craft-node]:hover { outline-color: rgba(59, 130, 246, 0.5); } /* `data-craft-hovered` is set (presence, no value) in RenderNode.tsx, mirroring Craft's own `hovered` node event. Craft.js's `connectors.connect()` (called by every component) wires that event to a native mouseover/mouseleave listener on each node's dom internally, so this rule is a real mouse-hover highlight for any node (container or leaf). Scoped under `.canvas-device-frame` and suppressed by `.guides-off` so "Show guides: off" stays airtight. */ .canvas-device-frame [data-craft-hovered] { outline: 2px dashed var(--color-accent) !important; outline-offset: -1px; } /* `data-layer-hovered` is set directly by the Layers panel (LayersPanel.tsx) on its own row hover/focus -- a separate producer (writing straight to the target's dom rather than through Craft's `hovered` event, since the action that drives it is stripped from the public `useEditor()` API at runtime) that drives a highlight when hovering a layer row. This is a deliberate locator action (not a structural drop-target guide), so it uses a SOLID outline -- visually distinct from the dashed structural guides -- and is intentionally NOT suppressed by `.guides-off`: hiding guides shouldn't blind the Layers-panel hover→canvas locator. */ .canvas-device-frame [data-layer-hovered] { outline: 2px solid var(--color-accent) !important; outline-offset: -1px; } /* "Show guides" topbar toggle (TopBar.tsx) -- flips this class on .canvas-device-frame to hide structural drop-target guides and the mouse-hover highlight at once. Default ON. `[data-layer-hovered]` is deliberately excluded -- see rule above. */ .canvas-device-frame.guides-off [data-craft-node], .canvas-device-frame.guides-off [data-craft-hovered] { outline: none !important; } /* Header/footer zone-preview separator (Canvas.tsx ZonePreview) -- a neutral dashed rule marking where the editable page meets the read-only header/footer preview. It's a structural guide, so it hides with the rest when "Show guides" is off. */ .canvas-device-frame.guides-off .zone-preview-sep { border-color: transparent !important; } /* -------------------------------------------------------------------------- Craft.js Selection Indicator -------------------------------------------------------------------------- */ .craftjs-renderer [data-selected="true"] { outline: 2px solid var(--color-accent) !important; outline-offset: -1px; } /* Component indicator badge (floats over the selected node via a portal -- position/top/left are set inline per-node, see RenderNode.tsx) */ .component-indicator { position: absolute; top: -22px; left: 0; display: flex; align-items: center; gap: 6px; padding: 2px 6px 2px 8px; font-size: 10px; font-weight: 600; color: #ffffff; background: var(--color-accent); border-radius: var(--radius-sm) var(--radius-sm) 0 0; white-space: nowrap; pointer-events: none; z-index: 10; } .component-indicator-parent-btn { display: inline-flex; align-items: center; justify-content: center; width: 14px; height: 14px; padding: 0; border: none; border-radius: 2px; background: rgba(255, 255, 255, 0.2); color: #ffffff; font-size: 8px; line-height: 1; cursor: pointer; pointer-events: auto; transition: background var(--transition-fast); } .component-indicator-parent-btn:hover { background: rgba(255, 255, 255, 0.4); } /* -------------------------------------------------------------------------- Scrollbar Styling -------------------------------------------------------------------------- */ .panel-left ::-webkit-scrollbar, .panel-right ::-webkit-scrollbar, .editor-canvas ::-webkit-scrollbar { width: 6px; height: 6px; } .panel-left ::-webkit-scrollbar-track, .panel-right ::-webkit-scrollbar-track, .editor-canvas ::-webkit-scrollbar-track { background: transparent; } .panel-left ::-webkit-scrollbar-thumb, .panel-right ::-webkit-scrollbar-thumb, .editor-canvas ::-webkit-scrollbar-thumb { background: var(--color-border); border-radius: 3px; } .panel-left ::-webkit-scrollbar-thumb:hover, .panel-right ::-webkit-scrollbar-thumb:hover, .editor-canvas ::-webkit-scrollbar-thumb:hover { background: var(--color-border-light); } /* Firefox scrollbar */ .panel-left, .panel-right, .editor-canvas { scrollbar-width: thin; scrollbar-color: var(--color-border) transparent; } /* -------------------------------------------------------------------------- Pages Panel — Header/Footer zone rows (Item 11) Compact rows in the same neutral surface language as the page list, with a pencil hint that only appears on hover instead of a permanently-loud "Editing" pill. -------------------------------------------------------------------------- */ .zone-row-pencil { opacity: 0; transition: opacity var(--transition-fast); } .zone-row:hover .zone-row-pencil { opacity: 1; } /* -------------------------------------------------------------------------- Tooltip -------------------------------------------------------------------------- */ [data-tooltip] { position: relative; } [data-tooltip]::after { content: attr(data-tooltip); position: absolute; bottom: calc(100% + 6px); left: 50%; transform: translateX(-50%); padding: 4px 8px; font-size: 11px; font-weight: 500; color: #ffffff; background: #27272a; border-radius: var(--radius-sm); white-space: nowrap; pointer-events: none; opacity: 0; transition: opacity var(--transition-fast); z-index: 1000; } [data-tooltip]:hover::after { opacity: 1; } /* Topbar sits flush against the top of the viewport (`body { overflow: hidden }`), so the default "tooltip above the trigger" placement gets clipped off-screen for topbar buttons -- flip those to render below instead. Left-panel/canvas tooltips keep the default above placement, which has room to render there. */ .topbar [data-tooltip]::after { bottom: auto; top: calc(100% + 6px); } /* -------------------------------------------------------------------------- Notifications / Toasts -------------------------------------------------------------------------- */ .toast-container { position: fixed; bottom: 16px; right: 16px; display: flex; flex-direction: column; gap: 8px; z-index: 9999; } .toast { display: flex; align-items: center; gap: 8px; padding: 10px 16px; font-size: 12px; color: var(--color-text); background: var(--color-bg-elevated); border: 1px solid var(--color-border); border-radius: var(--radius-md); box-shadow: 0 4px 16px rgba(0, 0, 0, 0.4); animation: toast-slide-in 0.25s ease; } .toast.success { border-left: 3px solid var(--color-success); } .toast.error { border-left: 3px solid var(--color-danger); } @keyframes toast-slide-in { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: translateY(0); } } /* -------------------------------------------------------------------------- Drag & Drop Visual Cues -------------------------------------------------------------------------- */ .drop-indicator { position: relative; } .drop-indicator::before { content: ''; position: absolute; inset: 0; border: 2px dashed var(--color-accent); border-radius: var(--radius-sm); background: var(--color-accent-subtle); pointer-events: none; z-index: 5; } /* -------------------------------------------------------------------------- Empty Canvas State -------------------------------------------------------------------------- */ .empty-canvas-hint { position: absolute; inset: 0; display: flex; flex-direction: column; align-items: center; justify-content: center; padding: 60px 20px; color: var(--color-text-dim); font-size: 13px; text-align: center; gap: 12px; /* Overlays the drop area without intercepting drags/clicks meant for the underlying (empty) canvas root -- see Canvas.tsx. */ pointer-events: none; } .empty-canvas-hint i { font-size: 32px; opacity: 0.2; } /* -------------------------------------------------------------------------- Settings Tabs (General / Style / Advanced) -------------------------------------------------------------------------- */ .settings-tabs { display: flex; border-bottom: 1px solid var(--color-border); padding: 0 8px; background: var(--color-bg-surface); flex-shrink: 0; } .settings-tabs button { flex: 1; padding: 8px 4px; background: none; border: none; border-bottom: 2px solid transparent; color: var(--color-text-muted); font-size: 11px; font-weight: 600; cursor: pointer; text-transform: uppercase; letter-spacing: 0.5px; transition: color var(--transition-fast), border-color var(--transition-fast); } .settings-tabs button.active { color: var(--color-accent); border-bottom-color: var(--color-accent); } .settings-tabs button:hover { color: var(--color-text); } .settings-content { padding: 12px; } /* -------------------------------------------------------------------------- Responsive: Collapse Panels on Narrow Screens -------------------------------------------------------------------------- */ @media (max-width: 1024px) { .panel-left { width: 220px; min-width: 220px; } .panel-right { width: 260px; min-width: 260px; } } @media (max-width: 768px) { /* ------------------------------------------------------------------------ Phase A -- Mobile Editor Chrome Gated entirely behind this breakpoint; desktop styles above are untouched. See useIsMobile() (same 768px breakpoint) for the React-side equivalent that swaps LeftPanel/RightPanel for MobilePanelBar, and collapses TopBar. ------------------------------------------------------------------------ */ /* Side panels are not rendered on mobile (EditorShell swaps them for the bottom tab bar) -- kept as a defensive fallback in case either is ever mounted at this width. */ .panel-left, .panel-right { display: none; } /* -------------------------------------------------------------------- Viewport sizing -- dynamic viewport height accounts for mobile browsers' collapsing URL bar; the unconditional 100vh set earlier in this file is the fallback for browsers without dvh support. -------------------------------------------------------------------- */ .editor-app { height: 100vh; height: 100dvh; padding-top: env(safe-area-inset-top, 0px); padding-left: env(safe-area-inset-left, 0px); padding-right: env(safe-area-inset-right, 0px); } .editor-container { height: calc(100vh - var(--topbar-height) - var(--mobile-tabbar-height) - env(safe-area-inset-bottom, 0px)); height: calc(100dvh - var(--topbar-height) - var(--mobile-tabbar-height) - env(safe-area-inset-bottom, 0px)); } /* Full-width canvas, safe-area-aware bottom padding so content never sits flush against the fixed bottom tab bar. */ .editor-canvas { padding: 12px; } /* -------------------------------------------------------------------- Topbar collapse (TopBar.tsx renders a distinct `.topbar-mobile` markup branch -- this is purely the styling for it) -------------------------------------------------------------------- */ .topbar-mobile { padding: 0 8px; padding-left: calc(8px + env(safe-area-inset-left, 0px)); padding-right: calc(8px + env(safe-area-inset-right, 0px)); gap: 4px; } .topbar-mobile .topbar-left { flex: 0 0 auto; } .topbar-mobile .topbar-right { flex: 1; justify-content: flex-end; gap: 4px; } .topbar-mobile .topbar-btn.icon-only { width: 44px; height: 44px; padding: 0; justify-content: center; touch-action: manipulation; } .topbar-mobile .topbar-divider { margin: 0 2px; } .topbar-btn.overflow-toggle.active { color: var(--color-accent); background: var(--color-accent-subtle); border-color: var(--color-accent); } /* -------------------------------------------------------------------- Full-screen modals (Modal.tsx -- TemplateModal/HeadCodeModal/ SitesmithModal share `.modal-backdrop`). Same tab-bar cutout as `.mobile-sheet-backdrop` below, and for the same reason: without it, the backdrop's default `inset: 0` sits ABOVE the tab bar (--z-modal > --z-tabbar) and swallows every tap there, so "open Templates -> tap a bottom tab" would silently just close Templates (a tap on the backdrop) instead of ALSO opening the tapped sheet. `!important` because Modal.tsx sets `bottom` via an inline style, which otherwise beats this class rule. The modal's own content still sits above the tab bar (it's a commit/cancel moment) -- only the dismiss-affordance backdrop stops short, exactly like the sheet backdrop. -------------------------------------------------------------------- */ .modal-backdrop { bottom: calc(var(--mobile-tabbar-height) + env(safe-area-inset-bottom, 0px)) !important; } /* -------------------------------------------------------------------- "⋯" overflow menu (TopBarOverflowMenu.tsx) -------------------------------------------------------------------- */ .topbar-overflow-menu { position: fixed; top: calc(var(--topbar-height) + env(safe-area-inset-top, 0px) + 6px); right: calc(8px + env(safe-area-inset-right, 0px)); z-index: var(--z-overflow); min-width: 220px; max-width: calc(100vw - 16px); max-height: calc(100dvh - var(--topbar-height) - 24px); overflow-y: auto; background: var(--color-bg-elevated); border: 1px solid var(--color-border); border-radius: var(--radius-lg); box-shadow: 0 8px 28px rgba(0, 0, 0, 0.5); padding: 8px; display: flex; flex-direction: column; gap: 4px; } .topbar-overflow-section { display: flex; align-items: center; justify-content: space-between; gap: 10px; padding: 6px 8px 10px; border-bottom: 1px solid var(--color-border); margin-bottom: 4px; } .topbar-overflow-label { font-size: 12px; color: var(--color-text-muted); white-space: nowrap; } .topbar-overflow-section .device-switcher { height: 44px; } .topbar-overflow-section .device-btn { width: 44px; height: 42px; touch-action: manipulation; } .topbar-overflow-item { display: flex; align-items: center; gap: 10px; width: 100%; min-height: 44px; padding: 8px 10px; font-size: 13px; font-weight: 500; color: var(--color-text); background: transparent; border: none; border-radius: var(--radius-md); cursor: pointer; text-align: left; touch-action: manipulation; } .topbar-overflow-item:hover, .topbar-overflow-item:active { background: var(--color-bg-hover); } .topbar-overflow-item.active { color: var(--color-accent); } .topbar-overflow-sitesmith { padding: 6px 4px 0; } .topbar-overflow-sitesmith .sitesmith-btn { width: 100%; justify-content: center; min-height: 44px; } /* -------------------------------------------------------------------- Bottom tab bar (MobilePanelBar.tsx) -------------------------------------------------------------------- */ .mobile-tab-bar { position: fixed; left: 0; right: 0; bottom: 0; /* Above .mobile-sheet-backdrop (400) -- the tab bar must stay clickable even while a sheet is open so tapping a different tab swaps sheets directly, instead of requiring a close-then-reopen round trip. The backdrop itself stops short of the tab bar (see its `bottom` offset below) so this is mostly belt-and-suspenders for the boundary. */ z-index: var(--z-tabbar); display: flex; height: calc(var(--mobile-tabbar-height) + env(safe-area-inset-bottom, 0px)); background: var(--color-bg-surface); border-top: 1px solid var(--color-border); padding-bottom: env(safe-area-inset-bottom, 0px); padding-left: env(safe-area-inset-left, 0px); padding-right: env(safe-area-inset-right, 0px); } .mobile-tab-btn { flex: 1; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 3px; min-height: 44px; font-size: 10px; font-weight: 600; color: var(--color-text-muted); background: transparent; border: none; touch-action: manipulation; } .mobile-tab-btn i { font-size: 17px; } .mobile-tab-btn.active { color: var(--color-accent); } /* -------------------------------------------------------------------- Mobile selection toolbar (MobileSelectionToolbar.tsx, Phase B item 2) Bottom-fixed just above the tab bar -- simplest, robust positioning that never has to track a scrolling/resizing node's DOM rect. Sits at --z-selection-toolbar (600), above the tab bar (500) and sheet backdrop (400) so it's never occluded by either, but well below --z-modal (5000). Hidden entirely (component returns null) whenever a sheet is open, so it never has to fight the sheet for the same screen space. -------------------------------------------------------------------- */ .mobile-selection-toolbar { position: fixed; left: 0; right: 0; bottom: calc(var(--mobile-tabbar-height) + env(safe-area-inset-bottom, 0px)); z-index: var(--z-selection-toolbar); display: flex; background: var(--color-bg-elevated); border-top: 1px solid var(--color-border); box-shadow: 0 -4px 16px rgba(0, 0, 0, 0.35); padding: 4px calc(2px + env(safe-area-inset-right, 0px)) 4px calc(2px + env(safe-area-inset-left, 0px)); } .mobile-selection-toolbar-btn { flex: 1; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 2px; min-height: 44px; padding: 4px 2px; font-size: 9px; font-weight: 600; line-height: 1; color: var(--color-text); background: transparent; border: none; border-radius: var(--radius-md); cursor: pointer; touch-action: manipulation; } .mobile-selection-toolbar-btn i { font-size: 15px; } .mobile-selection-toolbar-btn:disabled { color: var(--color-text-dim); opacity: 0.4; } .mobile-selection-toolbar-btn.danger { color: var(--color-danger); } .mobile-selection-toolbar-btn.danger.confirming { background: rgba(239, 68, 68, 0.15); } /* Fast-follow item 4: while the toolbar above is visible, give the canvas scroll area enough extra bottom padding to clear it (on top of the regular 12px `.editor-canvas` padding, not replacing it), so the last section of a short page is still reachable by scrolling instead of sitting permanently underneath the fixed toolbar. */ .editor-canvas.has-mobile-selection-toolbar { padding-bottom: calc(12px + var(--mobile-selection-toolbar-height)); } /* Fast-follow item 3: while the Styles sheet is open with a node selected, `MobileSelectionToolbar`'s scroll-into-view effect needs somewhere to scroll THE NODE TO even when it's the last thing on the page -- without extra room below it, the canvas is already at its max scrollTop and the node's top can't be brought up past wherever it naturally landed (verified live: the last section stayed stuck under the sheet). A full extra viewport's worth of bottom padding guarantees any node's top can always reach the very top of the canvas. Temporary/cosmetic cost (a stretch of blank canvas below the real content while this is active) is an acceptable trade for the node never being permanently unreachable. -------------------------------------------------------------------- */ .editor-canvas.has-mobile-styles-sheet { padding-bottom: 100vh; padding-bottom: 100dvh; } /* -------------------------------------------------------------------- Bottom sheet (BottomSheet.tsx) -------------------------------------------------------------------- */ .mobile-sheet-backdrop { position: fixed; top: 0; left: 0; right: 0; /* Stop above the bottom tab bar (rather than a full-viewport `inset: 0`) so the tab bar is never covered/blocked by the backdrop -- tapping a different tab while a sheet is open swaps sheets directly instead of being swallowed by the overlay sitting on top of it. `--keyboard-inset` (item 5, Phase B) is set inline by BottomSheet.tsx from `useVisualViewportInsets` -- 0px when no on-screen keyboard is open (the default below is the no-JS/unsupported-browser fallback), otherwise the keyboard's height, so the whole sheet lifts clear of it instead of the keyboard covering its lower portion. */ bottom: calc(var(--mobile-tabbar-height) + env(safe-area-inset-bottom, 0px) + var(--keyboard-inset, 0px)); background: rgba(0, 0, 0, 0.5); z-index: var(--z-sheet-backdrop); display: flex; align-items: flex-end; } .mobile-sheet { width: 100%; /* Capped a second time by the keyboard inset so the sheet's own max-height shrinks along with the lifted backdrop -- without this the sheet could still try to be 65dvh tall from its new (higher) bottom edge and overflow off the top of the screen. */ max-height: calc(65vh - var(--keyboard-inset, 0px)); max-height: calc(65dvh - var(--keyboard-inset, 0px)); background: var(--color-bg-surface); border-top-left-radius: 16px; border-top-right-radius: 16px; border: 1px solid var(--color-border); border-bottom: none; display: flex; flex-direction: column; overflow: hidden; } .mobile-sheet-handle-row { display: flex; justify-content: center; padding: 8px 0 4px; flex-shrink: 0; /* Swipe-to-dismiss (item 4, Phase B) tracks its own touchmove here -- `none` stops the browser's native scroll/rubber-band gesture from competing with it. */ touch-action: none; } .mobile-sheet-handle { width: 36px; height: 4px; border-radius: 2px; background: var(--color-border-light); } .mobile-sheet-header { display: flex; align-items: center; justify-content: space-between; padding: 4px 12px 10px 16px; border-bottom: 1px solid var(--color-border); flex-shrink: 0; /* Swipe-to-dismiss (item 4, Phase B) -- see .mobile-sheet-handle-row. */ touch-action: none; } .mobile-sheet-title { font-size: 14px; font-weight: 600; color: var(--color-text); } .mobile-sheet-close { width: 44px; height: 44px; display: inline-flex; align-items: center; justify-content: center; font-size: 16px; color: var(--color-text-muted); background: transparent; border: none; border-radius: var(--radius-md); cursor: pointer; touch-action: manipulation; } .mobile-sheet-body { flex: 1; overflow-y: auto; -webkit-overflow-scrolling: touch; padding: 12px 16px; } /* -------------------------------------------------------------------- Touch ergonomics inside sheets -- 16px inputs kill iOS's auto-zoom on focus; touch-action speeds up tap response on chrome that doesn't need native scroll-gesture handling. `!important` is required here: GuidedStyles/StylePanel inputs (see `inputStyle` in `panels/right/styles/shared.tsx`) carry an INLINE `font-size: 12px`, which otherwise beats this class rule on the Styles sheet -- the most-used mobile surface -- and iOS zooms in on focus. `.sitesmith-textarea` (ChatInput.tsx, inline font-size: 14) has the same problem even though the Sitesmith modal isn't a sheet. -------------------------------------------------------------------- */ .mobile-sheet-body button, .mobile-sheet-body .block-item { touch-action: manipulation; } .mobile-sheet-body input, .mobile-sheet-body select, .mobile-sheet-body textarea, .guided-input, .control-input, .control-select, .topbar input, .topbar textarea, .topbar select, .sitesmith-textarea { font-size: 16px !important; } .mobile-sheet-body .settings-tabs button { min-height: 44px; } .mobile-sheet-body .color-preset { width: 40px; height: 40px; } /* Layers panel rows (item 4) -- the reliable fallback selection path when a node is hard to hit precisely on the canvas by touch. Bumped to the 44px touch-target minimum; tapping a row already selects (LayerNode's `handleActivate`), unchanged. */ .mobile-sheet-body .layer-node-row { min-height: 44px; touch-action: manipulation; } /* -------------------------------------------------------------------- Templates modal: category tabs stay horizontally scrollable and never shrink below a comfortable touch target; close button enlarged to the 44px minimum (overriding its 32px inline style). -------------------------------------------------------------------- */ .template-modal-tabbar { gap: 8px; -webkit-overflow-scrolling: touch; } .template-modal-tab-btn { flex-shrink: 0; min-height: 44px; padding: 8px 16px !important; touch-action: manipulation; } .template-modal-close-btn { width: 44px !important; height: 44px !important; } }