diff --git a/docs/superpowers/plans/2026-03-23-resize-rotate-redesign.md b/docs/superpowers/plans/2026-03-23-resize-rotate-redesign.md
new file mode 100644
index 00000000..ea10e5c3
--- /dev/null
+++ b/docs/superpowers/plans/2026-03-23-resize-rotate-redesign.md
@@ -0,0 +1,869 @@
+# Resize & Rotate/Flip UX Redesign Implementation Plan
+
+> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
+
+**Goal:** Replace the before/after slider with intuitive side-by-side comparison for resize and live CSS preview for rotate/flip.
+
+**Architecture:** Frontend-only changes. Resize gets a tab-based settings panel (Presets/Custom Size/Scale) and a new SideBySideComparison result view. Rotate/flip gets live CSS transform preview on the ImageViewer with an "Apply" button. The tool-page.tsx conditionally renders the appropriate result view per tool. No backend changes.
+
+**Tech Stack:** React, TypeScript, Tailwind CSS, Zustand, lucide-react
+
+**Spec:** `docs/superpowers/specs/2026-03-23-resize-rotate-redesign.md`
+
+---
+
+### Task 1: Create SideBySideComparison Component
+
+**Files:**
+- Create: `apps/web/src/components/common/side-by-side-comparison.tsx`
+
+- [ ] **Step 1: Create the component file**
+
+```tsx
+import { useState } from "react";
+
+interface SideBySideComparisonProps {
+ beforeSrc: string;
+ afterSrc: string;
+ beforeSize?: number;
+ afterSize?: number;
+}
+
+function formatSize(bytes: number): string {
+ if (bytes < 1024) return `${bytes} B`;
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
+ return `${(bytes / (1024 * 1024)).toFixed(2)} MB`;
+}
+
+export function SideBySideComparison({
+ beforeSrc,
+ afterSrc,
+ beforeSize,
+ afterSize,
+}: SideBySideComparisonProps) {
+ const [beforeDims, setBeforeDims] = useState<{ w: number; h: number } | null>(null);
+ const [afterDims, setAfterDims] = useState<{ w: number; h: number } | null>(null);
+
+ const savingsPercent =
+ beforeSize && afterSize && beforeSize > 0
+ ? ((1 - afterSize / beforeSize) * 100).toFixed(1)
+ : null;
+
+ const checkerboard = {
+ backgroundImage: `linear-gradient(45deg, #ccc 25%, transparent 25%),
+ linear-gradient(-45deg, #ccc 25%, transparent 25%),
+ linear-gradient(45deg, transparent 75%, #ccc 75%),
+ linear-gradient(-45deg, transparent 75%, #ccc 75%)`,
+ backgroundSize: "16px 16px",
+ backgroundPosition: "0 0, 0 8px, 8px -8px, -8px 0px",
+ };
+
+ return (
+
+ {/* Side-by-side images */}
+
+ {/* Original */}
+
+
+ Original
+
+
+

{
+ const img = e.currentTarget;
+ setBeforeDims({ w: img.naturalWidth, h: img.naturalHeight });
+ }}
+ />
+
+
+ {beforeDims && (
+
+ {beforeDims.w} × {beforeDims.h}
+
+ )}
+ {beforeSize != null &&
{formatSize(beforeSize)}
}
+
+
+
+ {/* Resized */}
+
+
+ Resized
+
+
+

{
+ const img = e.currentTarget;
+ setAfterDims({ w: img.naturalWidth, h: img.naturalHeight });
+ }}
+ />
+
+
+ {afterDims && (
+
+ {afterDims.w} × {afterDims.h}
+
+ )}
+ {afterSize != null &&
{formatSize(afterSize)}
}
+
+
+
+
+ {/* Size savings */}
+ {savingsPercent !== null && (
+
0 ? "text-green-600 dark:text-green-400" : "text-red-500"}`}
+ >
+ {Number(savingsPercent) > 0
+ ? `${savingsPercent}% smaller`
+ : `${Math.abs(Number(savingsPercent))}% larger`}
+
+ )}
+
+ );
+}
+```
+
+- [ ] **Step 2: Verify it compiles**
+
+Run: `cd /Users/sidd/Desktop/Personal/Projects/Stirling-Image && pnpm --filter @stirling-image/web build 2>&1 | tail -5`
+Expected: Build succeeds
+
+- [ ] **Step 3: Commit**
+
+```bash
+git add apps/web/src/components/common/side-by-side-comparison.tsx
+git commit -m "feat: add SideBySideComparison component for resize results"
+```
+
+---
+
+### Task 2: Rewrite ResizeSettings with Tab-Based UI
+
+**Files:**
+- Modify: `apps/web/src/components/tools/resize-settings.tsx`
+
+- [ ] **Step 1: Rewrite resize-settings.tsx with three tabs**
+
+Replace the entire file content with:
+
+```tsx
+import { useState } from "react";
+import { SOCIAL_MEDIA_PRESETS } from "@stirling-image/shared";
+import { useFileStore } from "@/stores/file-store";
+import { useToolProcessor } from "@/hooks/use-tool-processor";
+import { Download, Link, Unlink } from "lucide-react";
+import { ProgressCard } from "@/components/common/progress-card";
+
+type ResizeTab = "presets" | "custom" | "scale";
+type FitMode = "cover" | "contain" | "fill";
+
+const FIT_LABELS: Record = {
+ cover: "Crop to fit",
+ contain: "Fit inside",
+ fill: "Stretch",
+};
+
+// Group presets by platform
+const platforms = [...new Set(SOCIAL_MEDIA_PRESETS.map((p) => p.platform))];
+
+export function ResizeSettings() {
+ const { files } = useFileStore();
+ const { processFiles, processing, error, downloadUrl, progress } =
+ useToolProcessor("resize");
+
+ const [tab, setTab] = useState("presets");
+ const [selectedPreset, setSelectedPreset] = useState(null);
+ const [width, setWidth] = useState("");
+ const [height, setHeight] = useState("");
+ const [percentage, setPercentage] = useState("50");
+ const [fit, setFit] = useState("cover");
+ const [lockAspect, setLockAspect] = useState(true);
+ const [withoutEnlargement, setWithoutEnlargement] = useState(false);
+
+ const handlePreset = (preset: (typeof SOCIAL_MEDIA_PRESETS)[number]) => {
+ const key = `${preset.platform}-${preset.name}`;
+ if (selectedPreset === key) {
+ setSelectedPreset(null);
+ setWidth("");
+ setHeight("");
+ } else {
+ setSelectedPreset(key);
+ setWidth(String(preset.width));
+ setHeight(String(preset.height));
+ }
+ };
+
+ const handleProcess = () => {
+ const settings: Record = {};
+
+ if (tab === "scale") {
+ settings.percentage = Number(percentage);
+ } else {
+ if (width) settings.width = Number(width);
+ if (height) settings.height = Number(height);
+ settings.fit = tab === "presets" ? "cover" : fit;
+ settings.withoutEnlargement = withoutEnlargement;
+ }
+
+ processFiles(files, settings);
+ };
+
+ const hasFile = files.length > 0;
+ const canProcess =
+ hasFile &&
+ !processing &&
+ (tab === "scale"
+ ? Number(percentage) > 0
+ : Boolean(width) || Boolean(height));
+
+ const handleSubmit = (e: React.FormEvent) => {
+ e.preventDefault();
+ if (canProcess) handleProcess();
+ };
+
+ const tabClass = (t: ResizeTab) =>
+ `flex-1 text-xs py-1.5 rounded ${tab === t ? "bg-primary text-primary-foreground" : "bg-muted text-muted-foreground"}`;
+
+ return (
+
+ );
+}
+```
+
+- [ ] **Step 2: Verify it compiles**
+
+Run: `cd /Users/sidd/Desktop/Personal/Projects/Stirling-Image && pnpm --filter @stirling-image/web build 2>&1 | tail -5`
+Expected: Build succeeds
+
+- [ ] **Step 3: Commit**
+
+```bash
+git add apps/web/src/components/tools/resize-settings.tsx
+git commit -m "feat: rewrite resize settings with tab-based UI (presets, custom, scale)"
+```
+
+---
+
+### Task 3: Add CSS Transform Props to ImageViewer
+
+**Files:**
+- Modify: `apps/web/src/components/common/image-viewer.tsx`
+
+- [ ] **Step 1: Add optional CSS transform props to the ImageViewer interface and apply them**
+
+Add `cssRotate`, `cssFlipH`, `cssFlipV` optional props to the `ImageViewerProps` interface. In the `imageStyle` computation, compose CSS transforms when these props are provided.
+
+Changes to make:
+
+1. Update the interface (line 5-8):
+
+```tsx
+interface ImageViewerProps {
+ src: string;
+ filename: string;
+ fileSize: number;
+ cssRotate?: number;
+ cssFlipH?: boolean;
+ cssFlipV?: boolean;
+}
+```
+
+2. Update the component destructuring (line 14):
+
+```tsx
+export function ImageViewer({ src, filename, fileSize, cssRotate, cssFlipH, cssFlipV }: ImageViewerProps) {
+```
+
+3. Update the `imageStyle` computation (lines 65-68) to compose CSS transforms:
+
+```tsx
+ const previewTransform = [
+ cssRotate ? `rotate(${cssRotate}deg)` : "",
+ cssFlipH ? "scaleX(-1)" : "",
+ cssFlipV ? "scaleY(-1)" : "",
+ ]
+ .filter(Boolean)
+ .join(" ");
+
+ const imageStyle =
+ fitMode === "fit"
+ ? {
+ maxWidth: "100%",
+ maxHeight: "100%",
+ objectFit: "contain" as const,
+ ...(previewTransform && { transform: previewTransform }),
+ }
+ : {
+ transform: `scale(${zoom / 100})${previewTransform ? ` ${previewTransform}` : ""}`,
+ transformOrigin: "center center",
+ };
+```
+
+- [ ] **Step 2: Verify it compiles**
+
+Run: `cd /Users/sidd/Desktop/Personal/Projects/Stirling-Image && pnpm --filter @stirling-image/web build 2>&1 | tail -5`
+Expected: Build succeeds
+
+- [ ] **Step 3: Commit**
+
+```bash
+git add apps/web/src/components/common/image-viewer.tsx
+git commit -m "feat: add CSS transform props to ImageViewer for live rotate/flip preview"
+```
+
+---
+
+### Task 4: Update RotateSettings with Live Preview Callback
+
+**Files:**
+- Modify: `apps/web/src/components/tools/rotate-settings.tsx`
+
+- [ ] **Step 1: Add onPreviewTransform callback prop and change button label**
+
+The component needs to:
+1. Accept an optional `onPreviewTransform` callback
+2. Call it on every state change (angle, flipH, flipV) via useEffect
+3. Change the submit button label from "Rotate" to "Apply"
+
+Replace the entire file:
+
+```tsx
+import { useState, useEffect } from "react";
+import { useFileStore } from "@/stores/file-store";
+import { useToolProcessor } from "@/hooks/use-tool-processor";
+import {
+ Download,
+ RotateCcw,
+ RotateCw,
+ FlipHorizontal,
+ FlipVertical,
+} from "lucide-react";
+import { ProgressCard } from "@/components/common/progress-card";
+
+export interface PreviewTransform {
+ rotate: number;
+ flipH: boolean;
+ flipV: boolean;
+}
+
+interface RotateSettingsProps {
+ onPreviewTransform?: (transform: PreviewTransform) => void;
+}
+
+export function RotateSettings({ onPreviewTransform }: RotateSettingsProps) {
+ const { files } = useFileStore();
+ const { processFiles, processing, error, downloadUrl, progress } =
+ useToolProcessor("rotate");
+
+ const [angle, setAngle] = useState(0);
+ const [flipH, setFlipH] = useState(false);
+ const [flipV, setFlipV] = useState(false);
+
+ // Emit preview transform on every change
+ useEffect(() => {
+ onPreviewTransform?.({ rotate: angle, flipH, flipV });
+ }, [angle, flipH, flipV, onPreviewTransform]);
+
+ const rotateLeft = () => setAngle((a) => (a - 90 + 360) % 360);
+ const rotateRight = () => setAngle((a) => (a + 90) % 360);
+
+ const handleProcess = () => {
+ processFiles(files, {
+ angle,
+ horizontal: flipH,
+ vertical: flipV,
+ });
+ };
+
+ const hasFile = files.length > 0;
+ const hasChanges = angle !== 0 || flipH || flipV;
+
+ const handleSubmit = (e: React.FormEvent) => {
+ e.preventDefault();
+ if (hasFile && hasChanges && !processing) handleProcess();
+ };
+
+ return (
+
+ );
+}
+```
+
+- [ ] **Step 2: Verify it compiles**
+
+Run: `cd /Users/sidd/Desktop/Personal/Projects/Stirling-Image && pnpm --filter @stirling-image/web build 2>&1 | tail -5`
+Expected: Build succeeds
+
+- [ ] **Step 3: Commit**
+
+```bash
+git add apps/web/src/components/tools/rotate-settings.tsx
+git commit -m "feat: add live preview callback to RotateSettings, rename button to Apply"
+```
+
+---
+
+### Task 5: Update tool-page.tsx for Conditional Rendering
+
+**Files:**
+- Modify: `apps/web/src/pages/tool-page.tsx`
+
+- [ ] **Step 1: Add imports, preview transform state, and conditional result rendering**
+
+Changes to make in `tool-page.tsx`:
+
+1. Add imports at the top (after existing imports, around line 8):
+
+```tsx
+import { SideBySideComparison } from "@/components/common/side-by-side-comparison";
+import type { PreviewTransform } from "@/components/tools/rotate-settings";
+```
+
+2. Add a set for tools that use alternate result views (after `NO_DROPZONE_TOOLS` on line 63):
+
+```tsx
+const SIDE_BY_SIDE_TOOLS = new Set(["resize"]);
+const LIVE_PREVIEW_TOOLS = new Set(["rotate"]);
+```
+
+3. Update `ToolSettingsPanel` to accept and pass through the preview transform callback (replace the function starting at line 65):
+
+```tsx
+function ToolSettingsPanel({
+ toolId,
+ onPreviewTransform,
+}: {
+ toolId: string;
+ onPreviewTransform?: (t: PreviewTransform) => void;
+}) {
+ // Phase 2: Core tools
+ if (toolId === "resize") return ;
+ if (toolId === "crop") return ;
+ if (toolId === "rotate") return ;
+```
+
+(Rest of ToolSettingsPanel stays identical)
+
+4. In the `ToolPage` component, add preview transform state (after `const [mobileSettingsOpen, setMobileSettingsOpen]` on line 177):
+
+```tsx
+const [previewTransform, setPreviewTransform] = useState(null);
+```
+
+5. Update ToolSettingsPanel usage in both mobile and desktop layouts — pass the callback:
+
+```tsx
+
+```
+
+6. Replace the result rendering logic in the main area for **both mobile and desktop layouts**. Replace the entire conditional block (from `{isNoDropzone ?` through the closing `}`). Apply this **identically** in both the mobile layout (around line 286) and the desktop layout (around line 372):
+
+```tsx
+{isNoDropzone ? (
+
+
Configure settings and generate.
+
+) : hasProcessed && originalBlobUrl && SIDE_BY_SIDE_TOOLS.has(tool.id) ? (
+
+) : hasProcessed && originalBlobUrl && LIVE_PREVIEW_TOOLS.has(tool.id) ? (
+
+) : hasProcessed && originalBlobUrl ? (
+
+) : hasFile && originalBlobUrl ? (
+
+) : (
+
+)}
+```
+
+**Important:** The `BeforeAfterSlider` is kept for all tools except resize (SideBySideComparison) and rotate (ImageViewer). The `BeforeAfterSlider` import must NOT be removed.
+
+- [ ] **Step 2: Verify it compiles**
+
+Run: `cd /Users/sidd/Desktop/Personal/Projects/Stirling-Image && pnpm --filter @stirling-image/web build 2>&1 | tail -5`
+Expected: Build succeeds
+
+- [ ] **Step 3: Commit**
+
+```bash
+git add apps/web/src/pages/tool-page.tsx
+git commit -m "feat: conditional result views — side-by-side for resize, live preview for rotate"
+```
+
+---
+
+### Task 6: Docker Rebuild and Test
+
+**Files:**
+- No file changes — build and run existing Docker setup
+
+- [ ] **Step 1: Build Docker image**
+
+```bash
+cd /Users/sidd/Desktop/Personal/Projects/Stirling-Image
+docker compose -f docker/docker-compose.yml build
+```
+
+- [ ] **Step 2: Start the container**
+
+```bash
+docker compose -f docker/docker-compose.yml up -d
+```
+
+- [ ] **Step 3: Verify the app is running**
+
+```bash
+curl -f http://localhost:1349/api/v1/health
+```
+
+Expected: Health check passes
+
+- [ ] **Step 4: Report to user for UI testing**
+
+App is running at `http://localhost:1349`. User can test:
+- Resize tool: tab-based settings (Presets, Custom Size, Scale), side-by-side result view
+- Rotate tool: live CSS preview as controls change, "Apply" button, processed result in standard viewer