/* ────────────────────────────────────────────────────────────────────────── * App layout — Pawfectly Yours * * Top-level structure: * .editor-shell * .ph-header (top, full width — defined in Header.css) * .editor-body (flex row) * .canvas-area (flex: 1) * .canvas-area__stage (the Konva canvas frame, fills space) * .canvas-area__bottom-bar (absolute, pinned to bottom) * .canvas-area__toolbar (left of the bar; desktop, when selected) * .canvas-area__right-stack (right cluster, stacked vertically) * .canvas-area__history (undo/redo pill, on top) * .canvas-area__zoom (zoom+snap pill, below) * .right-panel-wrap (fixed width, desktop only) * * On mobile the right panel becomes a bottom-sheet modal, the element * toolbar is pinned to the TOP of the canvas area (via .mobile-toolbar-wrap, * NOT via .canvas-area__bottom-bar), .canvas-area__bottom-bar flips to * static positioning to flow below the stage with the history + zoom * pills centered, and a FAB toggles the sheet. * ────────────────────────────────────────────────────────────────────────── */ .editor-shell { /* Definite viewport height — see the #root comment in index.css for * why this is `height` and not `min-height`. Removed the prior * `flex: 1` because with #root now `height: 100vh`, editor-shell * being its only child means a height-of-100vh matches naturally; * `flex: 1` + `min-height: 100vh` together used to coexist as * "grow to fill #root, but at least 100vh," which let the box stretch * past 100vh when content pushed for more. */ display: flex; flex-direction: column; height: 100vh; width: 100%; overflow: hidden; } .editor-body { flex: 1; display: flex; align-items: stretch; gap: 0.75rem; padding: 0 0.75rem 0.75rem; min-height: 0; overflow: hidden; } /* ── Canvas area ─────────────────────────────────────────────────────────── */ .canvas-area { position: relative; flex: 1; display: flex; align-items: stretch; justify-content: center; min-width: 0; min-height: 0; } .canvas-area__stage { flex: 1; position: relative; display: flex; align-items: center; justify-content: center; min-height: 0; min-width: 0; } /* Element toolbar + zoom/snap controls sit inside a shared bottom bar * (.canvas-area__bottom-bar) that's pinned to the bottom of canvas-area. * Bundling them into a single absolute-positioned flex row — instead * of having each one independently `position: absolute` — means they * can't collide as the canvas-area narrows: the previous implementation * had the toolbar at left:1rem and zoom at right:1rem, and once * canvas-area dipped below ~880px (toolbar 380 + zoom ~150 + gaps) they * overlapped. The bar's flex layout keeps them spaced cleanly and on a * single horizontal line until the narrow-screen media query flips * them to stacked. * * pointer-events trick: the bar visually spans the full canvas width, * but the user must be able to click through the empty middle to drag * elements near the bottom of the canvas. Setting `pointer-events: none` * on the bar and re-enabling it on the toolbar / zoom children gives us * that pass-through. */ .canvas-area__bottom-bar { position: absolute; left: 1rem; right: 1rem; bottom: 1rem; z-index: 10; display: flex; align-items: flex-end; gap: 0.75rem; pointer-events: none; } .canvas-area__toolbar { width: min(100%, 380px); pointer-events: auto; } /* History pill (undo/redo) and zoom pill share a vertical stack * wrapper on the right side of the bottom bar, with history sitting * ABOVE zoom. The previous layout had them side-by-side in a single * flex row — which read as "undo/redo is a secondary affordance * attached to zoom". Stacking gives undo/redo its own visual weight * (the more important of the two for editing flow) and is the * standard hierarchy used by Figma / Sketch / Adobe Express. * * `margin-left: auto` lives on the stack wrapper, not the children, * so the whole cluster gets pushed to the right edge whether the * left-side toolbar is present or not. `align-items: flex-end` * keeps both pills right-aligned within the column even if they * differ in width (history is 2 buttons, zoom is 4 — zoom is * naturally wider). The narrow-screen and mobile breakpoints below * flatten this back to a row to avoid eating extra vertical space * when the bar is already in column mode. * * pointer-events: none on the wrapper, auto on the children — same * pass-through pattern as the parent bottom bar. The wrapper has no * pixels of its own; the pills inside accept the clicks. */ .canvas-area__right-stack { display: flex; flex-direction: column; gap: 0.5rem; margin-left: auto; align-items: flex-end; pointer-events: none; } .canvas-area__history { pointer-events: auto; } .canvas-area__zoom { pointer-events: auto; } /* ── Right panel ─────────────────────────────────────────────────────────── */ .right-panel-wrap { width: var(--right-panel-width); flex-shrink: 0; background: var(--brand-cream); border: 1px solid var(--border); border-radius: var(--radius-xl); box-shadow: var(--shadow-sm); overflow: hidden; display: flex; flex-direction: column; min-height: 0; } /* ── Export toast ────────────────────────────────────────────────────────── */ /* Floats above the canvas in the top-left corner so it doesn't compete with * the toolbar (bottom-left) or zoom controls (bottom-right). */ .export-toast-wrap { position: absolute; left: 1rem; top: 1rem; z-index: 12; pointer-events: none; /* When the canvas hint is shown it occupies top-left too; the hint hides * itself once an element is selected, and the export toast only appears * during/after a save action — they rarely overlap visually. */ } .export-toast { display: inline-flex; align-items: center; gap: 0.6rem; padding: 0.65rem 0.9rem; border-radius: var(--radius-md); background: #fff; border: 1px solid var(--border); box-shadow: var(--shadow-md); font-size: 13px; font-weight: 600; color: var(--text-primary); pointer-events: auto; /* Shared keyframe in index.css — see S13 utility section. */ animation: fade-up-in 0.18s ease-out; } .export-toast--progress { border-color: var(--brand-pink); color: var(--brand-pink-strong); } .export-toast--success { border-color: #bbf7d0; background: #f0fdf4; color: #15803d; } .export-toast--error { border-color: #fecaca; background: #fef2f2; color: #b91c1c; } .export-toast__link { color: inherit; font-weight: 700; text-decoration: underline; text-underline-offset: 2px; } .export-toast__close { background: transparent; border: none; font-size: 14px; cursor: pointer; color: inherit; padding: 0 0 0 0.25rem; line-height: 1; } /* ── App-wide toast (replaces alert() calls) ─────────────────────────────── */ /* Centered at the top of the canvas area, distinct from the export-toast * which sits in the top-left. Auto-dismisses after a few seconds; the user * can also dismiss manually with the close button. Three kinds matching * the export-toast color language so the warning vocabulary stays * consistent: success = green, error = red, info = brand pink. */ .app-toast-wrap { position: absolute; top: 1rem; left: 50%; transform: translateX(-50%); z-index: 13; pointer-events: none; max-width: calc(100% - 2rem); } .app-toast { display: inline-flex; align-items: center; gap: 0.6rem; padding: 0.65rem 0.9rem; border-radius: var(--radius-md); background: #fff; border: 1px solid var(--border); box-shadow: var(--shadow-md); font-size: 13px; font-weight: 600; color: var(--text-primary); pointer-events: auto; animation: app-toast-in 0.18s ease-out; max-width: 100%; } .app-toast > span { /* Wrap long messages (e.g. the share-fallback URL toast) instead of * letting them stretch the toast off-screen. */ word-break: break-word; overflow-wrap: anywhere; } @keyframes app-toast-in { from { opacity: 0; transform: translateX(-50%) translateY(-4px); } to { opacity: 1; transform: translateX(-50%) translateY(0); } } .app-toast--success { border-color: #bbf7d0; background: #f0fdf4; color: #15803d; } .app-toast--error { border-color: #fecaca; background: #fef2f2; color: #b91c1c; } .app-toast--info { border-color: var(--brand-pink); background: var(--brand-pink-soft); color: var(--brand-pink-strong); } .app-toast__close { background: transparent; border: none; font-size: 14px; cursor: pointer; color: inherit; padding: 0 0 0 0.25rem; line-height: 1; } @media (max-width: 768px) { .app-toast-wrap { top: calc(var(--header-height-mobile) + 0.6rem); width: calc(100% - 1rem); } .app-toast { width: 100%; justify-content: center; } } /* ── FAB (mobile only) ───────────────────────────────────────────────────── */ .app-fab { position: fixed; right: 1rem; bottom: calc(1rem + env(safe-area-inset-bottom, 0px)); z-index: 700; width: 56px; height: 56px; border-radius: 999px; border: none; background: var(--brand-pink); color: #fff; display: inline-flex; align-items: center; justify-content: center; box-shadow: 0 10px 22px -6px rgba(236, 72, 153, 0.6), 0 4px 8px rgba(0, 0, 0, 0.1); transition: transform 0.12s ease, background 0.12s ease; } .app-fab:hover { background: var(--brand-pink-strong); } .app-fab:active { transform: scale(0.94); } /* ── Mobile element toolbar wrapper ──────────────────────────────────────── */ .mobile-toolbar-wrap { position: fixed; left: 50%; top: calc(var(--header-height-mobile) + 0.5rem); transform: translateX(-50%); z-index: 600; width: calc(100vw - 1.5rem); max-width: 380px; pointer-events: auto; } /* ── Responsive ──────────────────────────────────────────────────────────── */ /* Tablet — narrower right panel */ @media (max-width: 1100px) { .right-panel-wrap { width: var(--right-panel-width-md); } } /* Below the tablet breakpoint where the right panel might crowd the canvas: * keep the bottom bar pinned to the bottom of canvas-area (still snapped * to the screen bottom), but flip it to column direction so the toolbar * and zoom stack instead of competing for horizontal room. The toolbar's * 380px max width plus the zoom widget plus gaps adds up to roughly 540px; * canvas-area drops below that around 880px viewport width once the right * panel takes its share, which is what this breakpoint catches. */ @media (max-width: 900px) and (min-width: 769px) { .canvas-area__bottom-bar { flex-direction: column; align-items: center; gap: 0.5rem; } .canvas-area__toolbar { width: 100%; max-width: 380px; } /* Flatten the right-stack to a horizontal row at this breakpoint. * The parent bar is already column-direction, so a vertically- * stacked right-stack inside that would push the bottom bar tall * enough to compete with the canvas for vertical space. A row * keeps history + zoom on the same line, centered along with the * toolbar that sits above them. margin-left: auto becomes a no-op * in column-direction so we reset it for clarity. */ .canvas-area__right-stack { flex-direction: row; align-items: center; margin-left: 0; } .canvas-area__history, .canvas-area__zoom { margin-left: 0; } } /* Mobile — bottom sheet handles the right panel */ @media (max-width: 768px) { .editor-body { padding: 0 0.5rem 0.5rem; gap: 0.5rem; } .canvas-area { flex-direction: column; } .canvas-area__stage { flex: 1; } /* Mobile bottom bar — pulled out of absolute positioning so it * participates in canvas-area's flex column. Sits below the stage, * centered horizontally, with the bottom margin clearing the FAB. * History + zoom pills sit side-by-side, centered as a pair. * Reset the pointer-events:none / margin-left:auto from the desktop * defaults so clicks land normally and centering works. */ .canvas-area__bottom-bar { position: static; align-self: center; margin-bottom: calc(4.5rem + env(safe-area-inset-bottom, 0px)); margin-top: 0.4rem; flex-direction: row; align-items: center; justify-content: center; gap: 0.5rem; pointer-events: auto; } /* Element toolbar is rendered via .mobile-toolbar-wrap on mobile, * not via the bottom bar. */ .canvas-area__toolbar { display: none; } /* Same flatten as the 900px breakpoint above — mobile bottom bar * is a horizontal row, so the right-stack inside follows suit * rather than stacking vertically. */ .canvas-area__right-stack { flex-direction: row; align-items: center; margin-left: 0; } .canvas-area__history, .canvas-area__zoom { margin-left: 0; } .export-toast-wrap { left: 0.5rem; right: 0.5rem; top: calc(var(--header-height-mobile) + 0.6rem); } .export-toast { width: 100%; justify-content: center; } } /* Tighten on tiny screens */ @media (max-width: 480px) { .editor-body { padding: 0 0.4rem 0.4rem; } .app-fab { width: 52px; height: 52px; } }