diff --git a/src/components/DevToolkitApp.tsx b/src/components/DevToolkitApp.tsx
new file mode 100644
index 0000000..6231c18
--- /dev/null
+++ b/src/components/DevToolkitApp.tsx
@@ -0,0 +1,296 @@
+import React, { useEffect, useMemo, useRef, useState } from "react";
+import { GuidTool } from "./tools/GuidTool";
+import { Base64Tool } from "./tools/Base64Tool";
+
+/**
+ * Definition for a tool displayed in the sidebar.
+ */
+interface ToolDefinition {
+ id: string;
+ name: string;
+ description: string;
+ keywords: string[];
+ render: (props: ToolRenderProps) => React.ReactNode;
+}
+
+/**
+ * Props provided to tool renderers.
+ */
+interface ToolRenderProps {
+ onCopied: (message: string) => void;
+}
+
+/**
+ * Props for the ToolButton component.
+ */
+interface ToolButtonProps {
+ tool: ToolDefinition;
+ isActive: boolean;
+ onSelect: (id: string) => void;
+}
+
+/**
+ * Renders the icon made of two square blocks.
+ */
+const ToolIcon: React.FC = () => {
+ return (
+
+
+
+
+ );
+};
+
+/**
+ * Renders a sidebar tool button with ripple feedback.
+ */
+const ToolButton: React.FC = ({
+ tool,
+ isActive,
+ onSelect,
+}) => {
+ const [ripple, setRipple] = useState<{
+ x: number;
+ y: number;
+ key: number;
+ } | null>(null);
+
+ /**
+ * Triggers the ripple animation and selects the tool.
+ */
+ const handleClick = (event: React.MouseEvent) => {
+ const rect = event.currentTarget.getBoundingClientRect();
+ setRipple({
+ x: event.clientX - rect.left,
+ y: event.clientY - rect.top,
+ key: Date.now(),
+ });
+ onSelect(tool.id);
+ };
+
+ return (
+
+ );
+};
+
+/**
+ * Skeleton placeholder for tool content transitions.
+ */
+const ToolSkeleton: React.FC = () => {
+ return (
+
+ );
+};
+
+/**
+ * Main application shell for the Dev Toolkit.
+ */
+const DevToolkitApp: React.FC = () => {
+ const [search, setSearch] = useState("");
+ const [activeId, setActiveId] = useState("guid");
+ const [renderId, setRenderId] = useState("guid");
+ const [loading, setLoading] = useState(false);
+ const [typing, setTyping] = useState(false);
+ const [toast, setToast] = useState(null);
+ const [progress, setProgress] = useState(0);
+ const [loaded, setLoaded] = useState(false);
+ const contentRef = useRef(null);
+ const transitionRef = useRef(null);
+ const typingRef = useRef(null);
+
+ const tools = useMemo(
+ () => [
+ {
+ id: "guid",
+ name: "GUID Generator",
+ description:
+ "Fast, compliant GUID creation for workflows and API testing.",
+ keywords: ["guid", "uuid", "id"],
+ render: ({ onCopied }) => ,
+ },
+ {
+ id: "base64",
+ name: "Base64 Studio",
+ description: "Convert text and files into clean Base64 payloads.",
+ keywords: ["base64", "encode", "file"],
+ render: ({ onCopied }) => ,
+ },
+ ],
+ [],
+ );
+
+ const filteredTools = useMemo(() => {
+ const query = search.trim().toLowerCase();
+ if (!query) {
+ return tools;
+ }
+ return tools.filter((tool) => {
+ return (
+ tool.name.toLowerCase().includes(query) ||
+ tool.keywords.some((keyword) => keyword.includes(query))
+ );
+ });
+ }, [search, tools]);
+
+ const activeTool = tools.find((tool) => tool.id === renderId) ?? tools[0];
+ const activeIndex = Math.max(
+ 0,
+ filteredTools.findIndex((tool) => tool.id === activeId),
+ );
+
+ /**
+ * Handles tool selection with skeleton transitions.
+ */
+ const handleSelectTool = (id: string) => {
+ if (id === activeId) {
+ return;
+ }
+ setActiveId(id);
+ setLoading(true);
+ if (transitionRef.current) {
+ window.clearTimeout(transitionRef.current);
+ }
+ transitionRef.current = window.setTimeout(() => {
+ setRenderId(id);
+ setLoading(false);
+ }, 320);
+ };
+
+ /**
+ * Shows a short toast notification.
+ */
+ const notify = (message: string) => {
+ setToast(message);
+ window.setTimeout(() => setToast(null), 2100);
+ };
+
+ /**
+ * Tracks typing for the search animation.
+ */
+ const handleSearchChange = (value: string) => {
+ setSearch(value);
+ setTyping(true);
+ if (typingRef.current) {
+ window.clearTimeout(typingRef.current);
+ }
+ typingRef.current = window.setTimeout(() => setTyping(false), 500);
+ };
+
+ useEffect(() => {
+ const id = window.requestAnimationFrame(() => setLoaded(true));
+ return () => window.cancelAnimationFrame(id);
+ }, []);
+
+ useEffect(() => {
+ if (filteredTools.length === 0) {
+ return;
+ }
+ if (!filteredTools.some((tool) => tool.id === activeId)) {
+ handleSelectTool(filteredTools[0].id);
+ }
+ }, [activeId, filteredTools]);
+
+ useEffect(() => {
+ const node = contentRef.current;
+ if (!node) {
+ return;
+ }
+
+ /**
+ * Updates the scroll progress indicator based on the content scroll.
+ */
+ const handleScroll = () => {
+ const max = node.scrollHeight - node.clientHeight;
+ const ratio = max > 0 ? node.scrollTop / max : 0;
+ setProgress(Math.min(1, Math.max(0, ratio)));
+ };
+
+ handleScroll();
+ node.addEventListener("scroll", handleScroll);
+ return () => node.removeEventListener("scroll", handleScroll);
+ }, [renderId]);
+
+ return (
+
+
+
+
+
+
+
+
handleSearchChange(event.target.value)}
+ />
+
+
+
{activeTool.name}
+
{activeTool.description}
+
+
+ {loading ? (
+
+ ) : (
+ activeTool.render({ onCopied: notify })
+ )}
+
+
+
+ {toast ?
{toast}
: null}
+
+ );
+};
+
+export default DevToolkitApp;
diff --git a/src/components/hooks/useCopyToClipboard.ts b/src/components/hooks/useCopyToClipboard.ts
new file mode 100644
index 0000000..906c4cb
--- /dev/null
+++ b/src/components/hooks/useCopyToClipboard.ts
@@ -0,0 +1,59 @@
+import { useCallback, useState } from "react";
+
+/**
+ * Result of a clipboard copy attempt.
+ */
+export interface CopyState {
+ ok: boolean;
+}
+
+/**
+ * Provides a safe clipboard copy helper with UI-friendly state.
+ */
+export const useCopyToClipboard = () => {
+ const [isCopied, setIsCopied] = useState(false);
+
+ /**
+ * Resets the copied state after a short delay.
+ */
+ const resetCopied = useCallback(() => {
+ const timeout = window.setTimeout(() => setIsCopied(false), 1800);
+ return () => window.clearTimeout(timeout);
+ }, []);
+
+ /**
+ * Copies a string into the clipboard using the best available API.
+ */
+ const copy = useCallback(
+ async (value: string): Promise => {
+ if (!value) {
+ return { ok: false };
+ }
+
+ try {
+ if (navigator.clipboard?.writeText) {
+ await navigator.clipboard.writeText(value);
+ } else {
+ const textarea = document.createElement("textarea");
+ textarea.value = value;
+ textarea.setAttribute("readonly", "true");
+ textarea.style.position = "absolute";
+ textarea.style.left = "-9999px";
+ document.body.appendChild(textarea);
+ textarea.select();
+ document.execCommand("copy");
+ document.body.removeChild(textarea);
+ }
+
+ setIsCopied(true);
+ resetCopied();
+ return { ok: true };
+ } catch {
+ return { ok: false };
+ }
+ },
+ [resetCopied],
+ );
+
+ return { isCopied, copy };
+};
diff --git a/src/components/tools/Base64Tool.tsx b/src/components/tools/Base64Tool.tsx
new file mode 100644
index 0000000..6379250
--- /dev/null
+++ b/src/components/tools/Base64Tool.tsx
@@ -0,0 +1,109 @@
+import React, { useMemo, useState } from "react";
+import { ToolSection } from "../ui/ToolSection";
+import { ToolOutput } from "../ui/ToolOutput";
+
+/**
+ * Props for the Base64Tool component.
+ */
+export interface Base64ToolProps {
+ onCopied?: (message: string) => void;
+}
+
+/**
+ * Encodes text into Base64 with UTF-8 support.
+ */
+const encodeTextToBase64 = (value: string): string => {
+ if (!value) {
+ return "";
+ }
+
+ const encoded = new TextEncoder().encode(value);
+ let binary = "";
+ encoded.forEach((byte) => {
+ binary += String.fromCharCode(byte);
+ });
+ return btoa(binary);
+};
+
+/**
+ * Reads a file as Base64 and returns its data payload.
+ */
+const readFileAsBase64 = (file: File): Promise => {
+ return new Promise((resolve, reject) => {
+ const reader = new FileReader();
+ reader.onload = () => {
+ const result = typeof reader.result === "string" ? reader.result : "";
+ const [, payload] = result.split(",");
+ resolve(payload ?? "");
+ };
+ reader.onerror = () => reject(reader.error);
+ reader.readAsDataURL(file);
+ });
+};
+
+/**
+ * Renders the Base64 conversion tool for text and files.
+ */
+export const Base64Tool: React.FC = ({ onCopied }) => {
+ const [textInput, setTextInput] = useState("");
+ const [fileOutput, setFileOutput] = useState("");
+ const [fileMeta, setFileMeta] = useState("");
+
+ const textOutput = useMemo(() => encodeTextToBase64(textInput), [textInput]);
+
+ /**
+ * Handles file selection and Base64 extraction.
+ */
+ const handleFileChange = async (
+ event: React.ChangeEvent,
+ ) => {
+ const [file] = Array.from(event.target.files ?? []);
+ if (!file) {
+ setFileOutput("");
+ setFileMeta("");
+ return;
+ }
+
+ try {
+ const payload = await readFileAsBase64(file);
+ setFileOutput(payload);
+ setFileMeta(`${file.name} - ${(file.size / 1024).toFixed(1)} KB`);
+ } catch {
+ setFileOutput("");
+ setFileMeta("Failed to read file");
+ }
+ };
+
+ return (
+
+
+ Plain text
+
+
+ Upload file
+
+ {fileMeta ? {fileMeta}
: null}
+
+
+
+
+
+ );
+};
diff --git a/src/components/tools/GuidTool.tsx b/src/components/tools/GuidTool.tsx
new file mode 100644
index 0000000..ced8603
--- /dev/null
+++ b/src/components/tools/GuidTool.tsx
@@ -0,0 +1,66 @@
+import React, { useEffect, useState } from "react";
+import { ToolSection } from "../ui/ToolSection";
+import { ToolOutput } from "../ui/ToolOutput";
+
+/**
+ * Props for the GuidTool component.
+ */
+export interface GuidToolProps {
+ onCopied?: (message: string) => void;
+}
+
+/**
+ * Creates a GUID using the best available browser API.
+ */
+const createGuid = (): string => {
+ if (crypto?.randomUUID) {
+ return crypto.randomUUID();
+ }
+
+ const template = "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx";
+ return template.replace(/[xy]/g, (char) => {
+ const random = (Math.random() * 16) | 0;
+ const value = char === "x" ? random : (random & 0x3) | 0x8;
+ return value.toString(16);
+ });
+};
+
+/**
+ * Renders the GUID generator tool.
+ */
+export const GuidTool: React.FC = ({ onCopied }) => {
+ const [guid, setGuid] = useState("");
+
+ /**
+ * Generates a new GUID and updates the output.
+ */
+ const generateGuid = () => {
+ setGuid(createGuid());
+ };
+
+ useEffect(() => {
+ generateGuid();
+ }, []);
+
+ return (
+
+
+
+ Create compliant GUIDs with a single click.
+
+
+
+
+
+
+
+
+
+ );
+};
diff --git a/src/components/ui/CopyButton.tsx b/src/components/ui/CopyButton.tsx
new file mode 100644
index 0000000..2150018
--- /dev/null
+++ b/src/components/ui/CopyButton.tsx
@@ -0,0 +1,62 @@
+import React from "react";
+import { useCopyToClipboard } from "../hooks/useCopyToClipboard";
+
+/**
+ * Props for the CopyButton component.
+ */
+export interface CopyButtonProps {
+ value: string;
+ label?: string;
+ disabled?: boolean;
+ onCopied?: (message: string) => void;
+}
+
+/**
+ * Renders a copy-to-clipboard button with animated feedback.
+ */
+export const CopyButton: React.FC = ({
+ value,
+ label = "Copy",
+ disabled,
+ onCopied,
+}) => {
+ const { isCopied, copy } = useCopyToClipboard();
+
+ /**
+ * Handles the copy click and triggers toast feedback.
+ */
+ const handleCopy = async () => {
+ if (disabled) {
+ return;
+ }
+ const result = await copy(value);
+ if (result.ok) {
+ onCopied?.("Copied to clipboard");
+ }
+ };
+
+ return (
+
+ );
+};
diff --git a/src/components/ui/ToolOutput.tsx b/src/components/ui/ToolOutput.tsx
new file mode 100644
index 0000000..066a79a
--- /dev/null
+++ b/src/components/ui/ToolOutput.tsx
@@ -0,0 +1,51 @@
+import React, { useEffect, useMemo, useState } from "react";
+import { CopyButton } from "./CopyButton";
+
+/**
+ * Props for the ToolOutput component.
+ */
+export interface ToolOutputProps {
+ label: string;
+ value: string;
+ placeholder?: string;
+ onCopied?: (message: string) => void;
+}
+
+/**
+ * Displays tool output with copy support and animated feedback.
+ */
+export const ToolOutput: React.FC = ({
+ label,
+ value,
+ placeholder = "No output yet",
+ onCopied,
+}) => {
+ const [pop, setPop] = useState(false);
+
+ const displayValue = useMemo(
+ () => (value?.length ? value : placeholder),
+ [value, placeholder],
+ );
+
+ /**
+ * Triggers the pop animation when the output updates.
+ */
+ useEffect(() => {
+ if (!value) {
+ return;
+ }
+ setPop(true);
+ const timeout = window.setTimeout(() => setPop(false), 350);
+ return () => window.clearTimeout(timeout);
+ }, [value]);
+
+ return (
+
+
{label}
+
+ {displayValue}
+
+
+
+ );
+};
diff --git a/src/components/ui/ToolSection.tsx b/src/components/ui/ToolSection.tsx
new file mode 100644
index 0000000..319dd24
--- /dev/null
+++ b/src/components/ui/ToolSection.tsx
@@ -0,0 +1,26 @@
+import React from "react";
+
+/**
+ * Props for the ToolSection component.
+ */
+export interface ToolSectionProps {
+ title: string;
+ children: React.ReactNode;
+ className?: string;
+}
+
+/**
+ * Provides a styled container for tool content blocks.
+ */
+export const ToolSection: React.FC = ({
+ title,
+ children,
+ className,
+}) => {
+ return (
+
+ );
+};
diff --git a/src/layouts/Layout.astro b/src/layouts/Layout.astro
index f6b55d4..b9f0f32 100644
--- a/src/layouts/Layout.astro
+++ b/src/layouts/Layout.astro
@@ -1,3 +1,7 @@
+---
+import '../styles/global.css';
+---
+
@@ -6,9 +10,9 @@
- Astro Basics
+ Dev Toolkit
-
+
diff --git a/src/pages/index.astro b/src/pages/index.astro
index c04f360..f0a0658 100644
--- a/src/pages/index.astro
+++ b/src/pages/index.astro
@@ -1,11 +1,8 @@
---
-import Welcome from '../components/Welcome.astro';
import Layout from '../layouts/Layout.astro';
-
-// Welcome to Astro! Wondering what to do next? Check out the Astro documentation at https://docs.astro.build
-// Don't want to use any of this? Delete everything in this file, the `assets`, `components`, and `layouts` directories, and start fresh.
+import DevToolkitApp from '../components/DevToolkitApp';
---
-
+
diff --git a/src/styles/global.css b/src/styles/global.css
index a461c50..f4434ff 100644
--- a/src/styles/global.css
+++ b/src/styles/global.css
@@ -1 +1,673 @@
-@import "tailwindcss";
\ No newline at end of file
+@import "tailwindcss";
+@import url("https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400;500;600;700&family=DM+Mono:wght@400;500&display=swap");
+
+:root {
+ color-scheme: dark;
+ --bg: #202833;
+ --sidebar: #1b222c;
+ --surface: #2a3442;
+ --border: #344154;
+ --accent: #24e4a7;
+ --accent-muted: rgba(36, 228, 167, 0.5);
+ --text: #e7eef6;
+ --muted: #9fb0c2;
+ --glow: 0 0 0.75rem rgba(36, 228, 167, 0.35);
+ --shadow: 0 20px 50px rgba(10, 15, 20, 0.35);
+}
+
+*,
+*::before,
+*::after {
+ box-sizing: border-box;
+}
+
+body {
+ margin: 0;
+ min-height: 100vh;
+ background: var(--bg);
+ color: var(--text);
+ font-family: "Space Grotesk", "Segoe UI", sans-serif;
+ letter-spacing: 0.01em;
+}
+
+button,
+input,
+textarea {
+ font-family: inherit;
+}
+
+.app-shell {
+ display: grid;
+ grid-template-columns: 240px 1fr;
+ min-height: 100vh;
+ gap: 0;
+ animation: layoutReveal 0.7s ease-out both;
+}
+
+.sidebar {
+ background: var(--sidebar);
+ padding: 28px 22px;
+ border-right: 1px solid var(--border);
+ position: relative;
+ box-shadow: inset -1px 0 0 rgba(255, 255, 255, 0.02);
+ opacity: 0;
+ transform: translateX(-12px);
+ transition:
+ opacity 0.5s ease,
+ transform 0.5s ease;
+}
+
+.sidebar-header {
+ font-size: 15px;
+ font-weight: 600;
+ text-transform: uppercase;
+ letter-spacing: 0.18em;
+ color: var(--muted);
+ margin-bottom: 18px;
+}
+
+.tool-list {
+ position: relative;
+ display: flex;
+ flex-direction: column;
+ gap: 10px;
+}
+
+.tool-active-indicator {
+ position: absolute;
+ left: 6px;
+ right: 6px;
+ height: 54px;
+ border-radius: 16px;
+ background: rgba(36, 228, 167, 0.16);
+ border: 1px solid rgba(36, 228, 167, 0.3);
+ box-shadow: var(--glow);
+ transform: translateY(var(--active-offset, 0px));
+ transition: transform 0.35s ease;
+ pointer-events: none;
+}
+
+.tool-button {
+ position: relative;
+ border-radius: 16px;
+ border: 1px solid transparent;
+ background: transparent;
+ color: var(--text);
+ padding: 12px 14px;
+ display: flex;
+ align-items: center;
+ gap: 12px;
+ font-size: 15px;
+ font-weight: 500;
+ text-align: left;
+ transition:
+ transform 0.2s ease,
+ background 0.2s ease,
+ border 0.2s ease,
+ color 0.2s ease;
+ overflow: hidden;
+ min-height: 54px;
+}
+
+.tool-button:hover {
+ transform: translateY(-2px);
+ background: rgba(255, 255, 255, 0.04);
+ border-color: rgba(36, 228, 167, 0.3);
+ filter: brightness(1.05);
+}
+
+.tool-button:active {
+ transform: scale(0.97);
+}
+
+.tool-button::before {
+ content: "";
+ position: absolute;
+ left: 8px;
+ top: 50%;
+ width: 3px;
+ height: 0;
+ background: var(--accent);
+ border-radius: 999px;
+ transform: translateY(-50%);
+ transition: height 0.2s ease;
+ opacity: 0.7;
+}
+
+.tool-button:hover::before {
+ height: 60%;
+}
+
+.tool-button.is-active::before {
+ height: 70%;
+}
+
+.tool-button.is-active {
+ color: var(--accent);
+ font-weight: 600;
+}
+
+.tool-icon {
+ position: relative;
+ display: grid;
+ grid-template-columns: repeat(2, 14px);
+ gap: 6px;
+}
+
+.tool-icon span {
+ width: 14px;
+ height: 14px;
+ border-radius: 4px;
+ background: rgba(255, 255, 255, 0.15);
+ border: 1px solid rgba(255, 255, 255, 0.18);
+ transition:
+ transform 0.2s ease,
+ background 0.2s ease,
+ border 0.2s ease;
+}
+
+.tool-button:hover .tool-icon span {
+ transform: rotate(5deg) scale(1.1);
+ background: rgba(36, 228, 167, 0.35);
+ border-color: rgba(36, 228, 167, 0.6);
+}
+
+.tool-label {
+ transition:
+ transform 0.2s ease,
+ color 0.2s ease;
+}
+
+.tool-button:hover .tool-label {
+ transform: translateX(6px);
+}
+
+.tool-ripple {
+ position: absolute;
+ border-radius: 999px;
+ background: rgba(36, 228, 167, 0.25);
+ transform: translate(-50%, -50%);
+ animation: ripple 0.6s ease-out forwards;
+ pointer-events: none;
+}
+
+.main-panel {
+ position: relative;
+ padding: 28px 32px 32px;
+ background: var(--bg);
+ overflow: hidden;
+}
+
+.main-panel::before {
+ content: "";
+ position: absolute;
+ inset: 0;
+ background:
+ radial-gradient(
+ circle at 10% 20%,
+ rgba(36, 228, 167, 0.18),
+ transparent 50%
+ ),
+ radial-gradient(
+ circle at 90% 10%,
+ rgba(80, 140, 255, 0.12),
+ transparent 45%
+ ),
+ radial-gradient(circle at 50% 80%, rgba(30, 52, 74, 0.5), transparent 60%);
+ opacity: 0.85;
+ animation: ambientShift 14s ease-in-out infinite;
+ pointer-events: none;
+}
+
+.main-content {
+ position: relative;
+ z-index: 1;
+ border-radius: 24px;
+ background: rgba(42, 52, 66, 0.65);
+ border: 1px solid var(--border);
+ box-shadow: var(--shadow);
+ padding: 24px;
+ min-height: 80vh;
+ backdrop-filter: blur(10px);
+ display: flex;
+ flex-direction: column;
+ overflow: auto;
+ opacity: 0;
+ transform: translateY(12px);
+ transition:
+ opacity 0.6s ease,
+ transform 0.6s ease;
+}
+
+.search-wrap {
+ position: relative;
+ display: flex;
+ align-items: center;
+ gap: 10px;
+ margin-bottom: 22px;
+ width: fit-content;
+ transition: transform 0.3s ease;
+ opacity: 0;
+ transform: translateY(-6px);
+}
+
+.search-wrap[data-typing="true"]::after {
+ content: "";
+ position: absolute;
+ left: 48px;
+ right: 12px;
+ bottom: -6px;
+ height: 2px;
+ background: linear-gradient(90deg, transparent, var(--accent), transparent);
+ animation: typingPulse 1s ease-in-out infinite;
+}
+
+.search-input {
+ background: rgba(27, 34, 44, 0.7);
+ border: 1px solid var(--border);
+ border-radius: 999px;
+ padding: 10px 16px 10px 42px;
+ color: var(--text);
+ width: 220px;
+ transition:
+ width 0.3s ease,
+ box-shadow 0.3s ease,
+ border 0.3s ease,
+ background 0.3s ease;
+}
+
+.search-input::placeholder {
+ color: var(--muted);
+ transition:
+ transform 0.3s ease,
+ opacity 0.3s ease;
+}
+
+.search-input:focus {
+ outline: none;
+ width: 255px;
+ border-color: rgba(36, 228, 167, 0.55);
+ box-shadow: var(--glow);
+ background: rgba(42, 52, 66, 0.9);
+}
+
+.search-input:focus::placeholder {
+ transform: translateX(6px);
+ opacity: 0.65;
+}
+
+.search-icon {
+ position: absolute;
+ left: 14px;
+ width: 18px;
+ height: 18px;
+ color: var(--muted);
+ transition:
+ transform 0.3s ease,
+ color 0.3s ease;
+}
+
+.search-wrap:focus-within .search-icon {
+ transform: rotate(8deg) scale(1.08);
+ color: var(--accent);
+}
+
+.tool-header {
+ margin-bottom: 18px;
+ opacity: 0;
+ transform: translateX(20px);
+ transition:
+ opacity 0.4s ease,
+ transform 0.4s ease;
+}
+
+.tool-title {
+ font-size: 28px;
+ font-weight: 600;
+ letter-spacing: 0.01em;
+}
+
+.tool-subtitle {
+ color: var(--muted);
+ margin-top: 6px;
+}
+
+.tool-body {
+ flex: 1;
+ position: relative;
+}
+
+.tool-content {
+ animation: contentEnter 0.45s ease both;
+}
+
+.tool-content .tool-section {
+ animation: contentEnter 0.45s ease both;
+}
+
+.tool-content .tool-section:nth-child(1) {
+ animation-delay: 0.08s;
+}
+
+.tool-content .tool-section:nth-child(2) {
+ animation-delay: 0.16s;
+}
+
+.tool-content .tool-section:nth-child(3) {
+ animation-delay: 0.24s;
+}
+
+.tool-section {
+ background: rgba(27, 34, 44, 0.8);
+ border: 1px solid var(--border);
+ border-radius: 18px;
+ padding: 18px;
+ margin-bottom: 16px;
+ box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.02);
+}
+
+.tool-section-title {
+ font-weight: 600;
+ margin-bottom: 12px;
+ font-size: 16px;
+}
+
+.field-label {
+ font-size: 13px;
+ text-transform: uppercase;
+ letter-spacing: 0.14em;
+ color: var(--muted);
+ margin-bottom: 8px;
+}
+
+.textarea,
+.text-input {
+ width: 100%;
+ border-radius: 14px;
+ border: 1px solid var(--border);
+ background: rgba(24, 30, 40, 0.9);
+ color: var(--text);
+ padding: 12px 14px;
+ min-height: 46px;
+ transition:
+ border 0.2s ease,
+ box-shadow 0.2s ease;
+}
+
+.textarea {
+ min-height: 120px;
+ resize: vertical;
+ font-family: "DM Mono", "Cascadia Code", monospace;
+}
+
+.text-input:focus,
+.textarea:focus {
+ outline: none;
+ border-color: rgba(36, 228, 167, 0.55);
+ box-shadow: var(--glow);
+}
+
+.primary-button {
+ background: var(--accent);
+ color: #0f1b1f;
+ border: none;
+ border-radius: 999px;
+ padding: 10px 18px;
+ font-weight: 600;
+ transition:
+ transform 0.2s ease,
+ box-shadow 0.2s ease,
+ opacity 0.2s ease;
+ box-shadow: var(--glow);
+}
+
+.primary-button:disabled {
+ background: var(--accent-muted);
+ cursor: not-allowed;
+ opacity: 0.7;
+ box-shadow: none;
+}
+
+.primary-button:hover:not(:disabled) {
+ transform: translateY(-1px);
+}
+
+.output-block {
+ position: relative;
+ background: rgba(19, 24, 33, 0.95);
+ border: 1px solid var(--border);
+ border-radius: 16px;
+ padding: 14px;
+ font-family: "DM Mono", "Cascadia Code", monospace;
+ font-size: 14px;
+ color: var(--text);
+ white-space: pre-wrap;
+ word-break: break-word;
+}
+
+.output-pop {
+ animation: resultPop 0.35s ease both;
+}
+
+.copy-button {
+ position: absolute;
+ top: 12px;
+ right: 12px;
+ background: rgba(36, 228, 167, 0.18);
+ border: 1px solid rgba(36, 228, 167, 0.4);
+ color: var(--accent);
+ border-radius: 10px;
+ padding: 6px 10px;
+ font-size: 12px;
+ font-weight: 600;
+ display: inline-flex;
+ align-items: center;
+ gap: 6px;
+ transition:
+ background 0.2s ease,
+ transform 0.2s ease;
+}
+
+.copy-button:hover:not(:disabled) {
+ background: rgba(36, 228, 167, 0.28);
+}
+
+.copy-button:disabled {
+ opacity: 0.6;
+ cursor: not-allowed;
+}
+
+.copy-icon {
+ width: 14px;
+ height: 14px;
+ transition: transform 0.25s ease;
+}
+
+.copy-button[data-copied="true"] {
+ background: rgba(36, 228, 167, 0.35);
+}
+
+.skeleton {
+ border-radius: 12px;
+ background: linear-gradient(
+ 110deg,
+ rgba(42, 52, 66, 0.6) 8%,
+ rgba(60, 72, 88, 0.7) 18%,
+ rgba(42, 52, 66, 0.6) 33%
+ );
+ background-size: 200% 100%;
+ animation: shimmer 1.2s infinite;
+}
+
+.toast {
+ position: fixed;
+ left: 50%;
+ bottom: 28px;
+ transform: translateX(-50%);
+ background: rgba(36, 228, 167, 0.9);
+ color: #0e181c;
+ padding: 10px 16px;
+ border-radius: 999px;
+ font-weight: 600;
+ box-shadow: var(--glow);
+ animation: toastSlide 2s ease forwards;
+ z-index: 50;
+}
+
+.scroll-progress {
+ position: absolute;
+ top: 0;
+ left: 0;
+ height: 3px;
+ background: linear-gradient(
+ 90deg,
+ rgba(36, 228, 167, 0.9),
+ rgba(110, 200, 220, 0.7)
+ );
+ border-radius: 999px;
+ transition: width 0.2s ease;
+}
+
+.app-shell[data-loaded="true"] .sidebar {
+ opacity: 1;
+ transform: translateX(0);
+}
+
+.app-shell[data-loaded="true"] .main-content {
+ opacity: 1;
+ transform: translateY(0);
+ transition-delay: 150ms;
+}
+
+.app-shell[data-loaded="true"] .search-wrap {
+ opacity: 1;
+ transform: translateY(0);
+ transition-delay: 300ms;
+}
+
+.app-shell[data-loaded="true"] .tool-header {
+ opacity: 1;
+ transform: translateX(0);
+ transition-delay: 220ms;
+}
+
+@keyframes layoutReveal {
+ 0% {
+ opacity: 0;
+ transform: translateY(10px);
+ }
+ 100% {
+ opacity: 1;
+ transform: translateY(0);
+ }
+}
+
+@keyframes ambientShift {
+ 0% {
+ transform: translate3d(0, 0, 0);
+ }
+ 50% {
+ transform: translate3d(-2%, -1%, 0);
+ }
+ 100% {
+ transform: translate3d(0, 0, 0);
+ }
+}
+
+@keyframes ripple {
+ 0% {
+ opacity: 0.8;
+ width: 0;
+ height: 0;
+ }
+ 100% {
+ opacity: 0;
+ width: 220px;
+ height: 220px;
+ }
+}
+
+@keyframes typingPulse {
+ 0% {
+ opacity: 0.4;
+ transform: translateX(-10%);
+ }
+ 50% {
+ opacity: 1;
+ transform: translateX(10%);
+ }
+ 100% {
+ opacity: 0.4;
+ transform: translateX(-10%);
+ }
+}
+
+@keyframes contentEnter {
+ 0% {
+ opacity: 0;
+ transform: translateX(30px);
+ }
+ 100% {
+ opacity: 1;
+ transform: translateX(0);
+ }
+}
+
+@keyframes shimmer {
+ 0% {
+ background-position: -200% 0;
+ }
+ 100% {
+ background-position: 200% 0;
+ }
+}
+
+@keyframes resultPop {
+ 0% {
+ opacity: 0;
+ transform: scale(0.98);
+ }
+ 100% {
+ opacity: 1;
+ transform: scale(1);
+ }
+}
+
+@keyframes toastSlide {
+ 0% {
+ opacity: 0;
+ transform: translate(-50%, 10px);
+ }
+ 20% {
+ opacity: 1;
+ transform: translate(-50%, 0);
+ }
+ 80% {
+ opacity: 1;
+ transform: translate(-50%, 0);
+ }
+ 100% {
+ opacity: 0;
+ transform: translate(-50%, 12px);
+ }
+}
+
+@media (max-width: 960px) {
+ .app-shell {
+ grid-template-columns: 1fr;
+ }
+
+ .sidebar {
+ border-right: none;
+ border-bottom: 1px solid var(--border);
+ }
+
+ .tool-list {
+ flex-direction: row;
+ flex-wrap: wrap;
+ }
+
+ .tool-active-indicator {
+ display: none;
+ }
+}