Index: frontend/package-lock.json
===================================================================
--- frontend/package-lock.json	(revision 4f85b41e4eaa6d668912791fb3cff6efbe7dc1a4)
+++ frontend/package-lock.json	(revision e9a0543bbb622a2b343d1f5fe3b2df7ddb6a0bd3)
@@ -9,4 +9,7 @@
       "version": "0.0.0",
       "dependencies": {
+        "@dnd-kit/core": "^6.3.1",
+        "@dnd-kit/sortable": "^10.0.0",
+        "@dnd-kit/utilities": "^3.2.2",
         "@relume_io/relume-tailwind": "^1.3.0",
         "@relume_io/relume-ui": "^1.3.1",
@@ -337,4 +340,57 @@
       "integrity": "sha512-P5LUNhtbj6YfI3iJjw5EL9eUAG6OitD0W3fWQcpQjDRc/QIsL0tRNuO1PcDvPccWL1fSTXXdE1ds+l95DV/OFA==",
       "license": "MIT"
+    },
+    "node_modules/@dnd-kit/accessibility": {
+      "version": "3.1.1",
+      "resolved": "https://registry.npmjs.org/@dnd-kit/accessibility/-/accessibility-3.1.1.tgz",
+      "integrity": "sha512-2P+YgaXF+gRsIihwwY1gCsQSYnu9Zyj2py8kY5fFvUM1qm2WA2u639R6YNVfU4GWr+ZM5mqEsfHZZLoRONbemw==",
+      "license": "MIT",
+      "dependencies": {
+        "tslib": "^2.0.0"
+      },
+      "peerDependencies": {
+        "react": ">=16.8.0"
+      }
+    },
+    "node_modules/@dnd-kit/core": {
+      "version": "6.3.1",
+      "resolved": "https://registry.npmjs.org/@dnd-kit/core/-/core-6.3.1.tgz",
+      "integrity": "sha512-xkGBRQQab4RLwgXxoqETICr6S5JlogafbhNsidmrkVv2YRs5MLwpjoF2qpiGjQt8S9AoxtIV603s0GIUpY5eYQ==",
+      "license": "MIT",
+      "dependencies": {
+        "@dnd-kit/accessibility": "^3.1.1",
+        "@dnd-kit/utilities": "^3.2.2",
+        "tslib": "^2.0.0"
+      },
+      "peerDependencies": {
+        "react": ">=16.8.0",
+        "react-dom": ">=16.8.0"
+      }
+    },
+    "node_modules/@dnd-kit/sortable": {
+      "version": "10.0.0",
+      "resolved": "https://registry.npmjs.org/@dnd-kit/sortable/-/sortable-10.0.0.tgz",
+      "integrity": "sha512-+xqhmIIzvAYMGfBYYnbKuNicfSsk4RksY2XdmJhT+HAC01nix6fHCztU68jooFiMUB01Ky3F0FyOvhG/BZrWkg==",
+      "license": "MIT",
+      "dependencies": {
+        "@dnd-kit/utilities": "^3.2.2",
+        "tslib": "^2.0.0"
+      },
+      "peerDependencies": {
+        "@dnd-kit/core": "^6.3.0",
+        "react": ">=16.8.0"
+      }
+    },
+    "node_modules/@dnd-kit/utilities": {
+      "version": "3.2.2",
+      "resolved": "https://registry.npmjs.org/@dnd-kit/utilities/-/utilities-3.2.2.tgz",
+      "integrity": "sha512-+MKAJEOfaBe5SmV6t34p80MMKhjvUz0vRrvVJbPT0WElzaOJ/1xs+D+KDv+tD/NE5ujfrChEcshd4fLn0wpiqg==",
+      "license": "MIT",
+      "dependencies": {
+        "tslib": "^2.0.0"
+      },
+      "peerDependencies": {
+        "react": ">=16.8.0"
+      }
     },
     "node_modules/@esbuild/aix-ppc64": {
Index: frontend/package.json
===================================================================
--- frontend/package.json	(revision 4f85b41e4eaa6d668912791fb3cff6efbe7dc1a4)
+++ frontend/package.json	(revision e9a0543bbb622a2b343d1f5fe3b2df7ddb6a0bd3)
@@ -11,4 +11,7 @@
   },
   "dependencies": {
+    "@dnd-kit/core": "^6.3.1",
+    "@dnd-kit/sortable": "^10.0.0",
+    "@dnd-kit/utilities": "^3.2.2",
     "@relume_io/relume-tailwind": "^1.3.0",
     "@relume_io/relume-ui": "^1.3.1",
Index: frontend/src/App.jsx
===================================================================
--- frontend/src/App.jsx	(revision 4f85b41e4eaa6d668912791fb3cff6efbe7dc1a4)
+++ frontend/src/App.jsx	(revision e9a0543bbb622a2b343d1f5fe3b2df7ddb6a0bd3)
@@ -20,4 +20,5 @@
 import Discipline from "./pages/Dashboard/pages/Discipline/Discipline.jsx";
 import DisciplineTracking from "./pages/Dashboard/pages/Discipline/DisciplineTracking.jsx";
+import CustomCategoryKanban from "./pages/Dashboard/pages/CustomCategory/CustomCategoryKanban.jsx";
 
 function RequireAuth({ children }) {
@@ -61,4 +62,5 @@
           <Route path="discipline" element={<Discipline />} />
            <Route path="discipline/tracking" element={<DisciplineTracking />} />
+           <Route path="custom/:customTrackingId" element={<CustomCategoryKanban />} />
         </Route>
         <Route path="*" element={<Navigate to="/" replace />} />
Index: frontend/src/api/discipline.js
===================================================================
--- frontend/src/api/discipline.js	(revision 4f85b41e4eaa6d668912791fb3cff6efbe7dc1a4)
+++ frontend/src/api/discipline.js	(revision e9a0543bbb622a2b343d1f5fe3b2df7ddb6a0bd3)
@@ -36,2 +36,53 @@
 }
 
+export async function getCustomTrackingCategories() {
+  const res = await api.get("/discipline/custom-tracking-categories");
+  return res?.data;
+}
+
+export async function createCustomTrackingCategory(payload) {
+  const res = await api.post("/discipline/custom-tracking-categories", payload);
+  return res?.data;
+}
+
+export async function getCustomCategoryTasks(customTrackingId) {
+  const res = await api.get(
+    `/discipline/custom-tracking-categories/${customTrackingId}/tasks`
+  );
+  return res?.data;
+}
+
+export async function createCustomCategoryTask(customTrackingId, payload) {
+  const res = await api.post(
+    `/discipline/custom-tracking-categories/${customTrackingId}/tasks`,
+    payload
+  );
+  return res?.data;
+}
+
+export async function updateCustomCategoryTask(customTrackingId, taskId, payload) {
+  const res = await api.put(
+    `/discipline/custom-tracking-categories/${customTrackingId}/tasks/${taskId}`,
+    payload
+  );
+  return res?.data;
+}
+
+export async function updateCustomCategoryTaskFinished(
+  customTrackingId,
+  taskId,
+  isFinished
+) {
+  const res = await api.patch(
+    `/discipline/custom-tracking-categories/${customTrackingId}/tasks/${taskId}/finished`,
+    { isFinished }
+  );
+  return res?.data;
+}
+
+export async function deleteCustomCategoryTask(customTrackingId, taskId) {
+  return api.delete(
+    `/discipline/custom-tracking-categories/${customTrackingId}/tasks/${taskId}`
+  );
+}
+
Index: frontend/src/pages/Dashboard/components/AddCustomCategoryModal.jsx
===================================================================
--- frontend/src/pages/Dashboard/components/AddCustomCategoryModal.jsx	(revision e9a0543bbb622a2b343d1f5fe3b2df7ddb6a0bd3)
+++ frontend/src/pages/Dashboard/components/AddCustomCategoryModal.jsx	(revision e9a0543bbb622a2b343d1f5fe3b2df7ddb6a0bd3)
@@ -0,0 +1,101 @@
+import React, { useEffect, useMemo, useState } from "react";
+import { createPortal } from "react-dom";
+
+export default function AddCustomCategoryModal({ open, onClose, onSubmit }) {
+  const [name, setName] = useState("");
+  const [submitting, setSubmitting] = useState(false);
+  const [error, setError] = useState(null);
+
+  // Reset the form whenever the modal is opened.
+  // NOTE: Hooks must be called in the same order on every render; do not early-return before hooks.
+  useEffect(() => {
+    if (!open) return;
+    setName("");
+    setError(null);
+    setSubmitting(false);
+  }, [open]);
+
+  const canSubmit = useMemo(() => {
+    return name.trim().length > 0 && name.trim().length <= 100 && !submitting;
+  }, [name, submitting]);
+
+  async function handleSubmit(e) {
+    e.preventDefault();
+    if (!canSubmit) return;
+
+    setSubmitting(true);
+    setError(null);
+    try {
+      await onSubmit({ name: name.trim() });
+      onClose?.();
+    } catch (err) {
+      setError(
+        err?.response?.data?.message ||
+          err?.message ||
+          "Failed to create custom category"
+      );
+      setSubmitting(false);
+    }
+  }
+
+  if (!open) return null;
+
+  return createPortal(
+    <div className="fixed inset-0 z-[100] flex items-center justify-center p-4">
+      <button
+        type="button"
+        className="absolute inset-0 bg-black/60"
+        aria-label="Close modal"
+        onClick={onClose}
+      />
+
+      <div className="relative w-full max-w-md rounded-xl bg-neutral-900 p-5 text-white shadow-xl">
+        <div className="flex items-center justify-between gap-4">
+          <h2 className="text-lg font-semibold">Add Custom Category</h2>
+          <button
+            type="button"
+            onClick={onClose}
+            className="rounded-md px-2 py-1 text-sm text-white/80 hover:bg-white/10"
+          >
+            Close
+          </button>
+        </div>
+
+        <form onSubmit={handleSubmit} className="mt-4 space-y-3">
+          <div>
+            <label className="mb-1 block text-sm text-white/80">Name</label>
+            <input
+              value={name}
+              onChange={(e) => setName(e.target.value)}
+              maxLength={100}
+              autoFocus
+              className="w-full rounded-lg border border-white/10 bg-black/40 px-3 py-2 text-sm outline-none focus:border-green-400"
+              placeholder="e.g. My Habit Tracking"
+            />
+            <div className="mt-1 text-xs text-white/50">
+              {name.trim().length}/100
+            </div>
+          </div>
+
+          {error ? (
+            <div className="rounded-lg border border-red-500/30 bg-red-500/10 p-2 text-sm text-red-200">
+              {error}
+            </div>
+          ) : null}
+
+          <button
+            type="submit"
+            disabled={!canSubmit}
+            className="w-full rounded-lg bg-green-400 px-3 py-2 text-sm font-medium text-black disabled:cursor-not-allowed disabled:opacity-60"
+          >
+            {submitting ? "Creating..." : "Create"}
+          </button>
+        </form>
+      </div>
+    </div>,
+    document.body
+  );
+}
+
+
+
Index: frontend/src/pages/Dashboard/components/DashboardSidebar.jsx
===================================================================
--- frontend/src/pages/Dashboard/components/DashboardSidebar.jsx	(revision 4f85b41e4eaa6d668912791fb3cff6efbe7dc1a4)
+++ frontend/src/pages/Dashboard/components/DashboardSidebar.jsx	(revision e9a0543bbb622a2b343d1f5fe3b2df7ddb6a0bd3)
@@ -1,3 +1,3 @@
-import React from "react";
+import React, { useEffect, useMemo, useState } from "react";
 import { NavLink, useLocation } from "react-router-dom";
 
@@ -20,7 +20,19 @@
   MdChecklist,
   MdAdd,
+  MdBook,
+  MdFlag,
+  MdStar,
+  MdBolt,
+  MdNoteAlt,
+  MdAutoGraph,
 } from "react-icons/md";
 
 import logo from "../../../assets/logo.png";
+
+import AddCustomCategoryModal from "./AddCustomCategoryModal.jsx";
+import {
+  createCustomTrackingCategory,
+  getCustomTrackingCategories,
+} from "../../../api/discipline.js";
 
 const navItems = [
@@ -33,6 +45,45 @@
 ];
 
+const CUSTOM_ICONS = [MdBook, MdFlag, MdStar, MdBolt, MdNoteAlt, MdAutoGraph];
+
+function stableIconForId(id) {
+  const n = Number(id);
+  if (!Number.isFinite(n)) return CUSTOM_ICONS[0];
+  return CUSTOM_ICONS[Math.abs(n) % CUSTOM_ICONS.length];
+}
+
 const DashboardSidebar = () => {
   const location = useLocation();
+  const [customOpen, setCustomOpen] = useState(false);
+  const [customCategories, setCustomCategories] = useState([]);
+
+  useEffect(() => {
+    let mounted = true;
+    (async () => {
+      try {
+        const res = await getCustomTrackingCategories();
+        const items = res?.items ?? [];
+        if (mounted) setCustomCategories(items);
+      } catch {
+        // If the user isn't authenticated yet or the endpoint fails,
+        // keep the sidebar usable (no custom categories).
+        if (mounted) setCustomCategories([]);
+      }
+    })();
+    return () => {
+      mounted = false;
+    };
+  }, []);
+
+  const customNavItems = useMemo(() => {
+    return (customCategories ?? []).map((c) => {
+      const Icon = stableIconForId(c.customTrackingId);
+      return {
+        title: c.name,
+        to: `/dashboard/custom/${c.customTrackingId}`,
+        icon: Icon,
+      };
+    });
+  }, [customCategories]);
 
   return (
@@ -52,4 +103,46 @@
           {navItems.map((item) => (
             <SidebarMenuItem key={item.title}>
+              {(() => {
+                const isActive =
+                  location.pathname === item.to ||
+                  location.pathname.startsWith(`${item.to}/`);
+
+                return (
+                  <SidebarMenuButton
+                    asChild
+                    isActive={isActive}
+                    className={
+                      isActive
+                        ? "!bg-green-400 !text-black hover:!bg-green-400 active:!bg-green-400"
+                        : ""
+                    }
+                  >
+                    <NavLink
+                      to={item.to}
+                      className="flex w-full items-center gap-3"
+                    >
+                      <item.icon
+                        className={
+                          isActive
+                            ? "size-6 shrink-0 text-black"
+                            : "size-6 shrink-0"
+                        }
+                      />
+                      <span className={isActive ? "text-black" : ""}>
+                        {item.title}
+                      </span>
+                    </NavLink>
+                  </SidebarMenuButton>
+                );
+              })()}
+            </SidebarMenuItem>
+          ))}
+
+          {customNavItems.length > 0 ? (
+            <div className="my-4 border-t border-white/10" />
+          ) : null}
+
+          {customNavItems.map((item) => (
+            <SidebarMenuItem key={`custom-${item.to}`}>
               {(() => {
                 const isActive =
@@ -94,4 +187,5 @@
             className="w-full justify-start gap-2 !bg-green-400 !text-black hover:!bg-green-500"
             type="button"
+            onClick={() => setCustomOpen(true)}
           >
             <MdAdd className="size-5" />
@@ -99,4 +193,13 @@
           </Button>
         </div>
+
+        <AddCustomCategoryModal
+          open={customOpen}
+          onClose={() => setCustomOpen(false)}
+          onSubmit={async (payload) => {
+            const created = await createCustomTrackingCategory(payload);
+            setCustomCategories((prev) => [created, ...(prev ?? [])]);
+          }}
+        />
       </SidebarContent>
     </Sidebar>
Index: frontend/src/pages/Dashboard/pages/CustomCategory/CustomCategoryKanban.jsx
===================================================================
--- frontend/src/pages/Dashboard/pages/CustomCategory/CustomCategoryKanban.jsx	(revision e9a0543bbb622a2b343d1f5fe3b2df7ddb6a0bd3)
+++ frontend/src/pages/Dashboard/pages/CustomCategory/CustomCategoryKanban.jsx	(revision e9a0543bbb622a2b343d1f5fe3b2df7ddb6a0bd3)
@@ -0,0 +1,128 @@
+import React, { useEffect, useMemo, useState } from "react";
+import { useParams } from "react-router-dom";
+
+import {
+  createCustomCategoryTask,
+  deleteCustomCategoryTask,
+  getCustomCategoryTasks,
+  getCustomTrackingCategories,
+  updateCustomCategoryTask,
+  updateCustomCategoryTaskFinished,
+} from "../../../../api/discipline.js";
+
+import CustomCategoryKanbanBoard from "./components/CustomCategoryKanbanBoard";
+
+export default function CustomCategoryKanban() {
+  const params = useParams();
+  const customTrackingId = Number(params.customTrackingId);
+
+  const [loading, setLoading] = useState(true);
+  const [categoryName, setCategoryName] = useState("Custom Category");
+  const [tasks, setTasks] = useState([]);
+  const [error, setError] = useState(null);
+
+  const lanes = useMemo(() => {
+    const notDone = (tasks ?? []).filter((t) => !t.isFinished);
+    const done = (tasks ?? []).filter((t) => t.isFinished);
+    return { notDone, done };
+  }, [tasks]);
+
+  async function refresh() {
+    setLoading(true);
+    setError(null);
+    try {
+      const [catsRes, tasksRes] = await Promise.all([
+        getCustomTrackingCategories(),
+        getCustomCategoryTasks(customTrackingId),
+      ]);
+
+      const cats = catsRes?.items ?? [];
+      const found = cats.find((c) => Number(c.customTrackingId) === customTrackingId);
+      setCategoryName(found?.name ?? "Custom Category");
+
+      setTasks(tasksRes?.tasks ?? []);
+    } catch (e) {
+      setError(e?.response?.data?.message || e?.message || "Failed to load category");
+    } finally {
+      setLoading(false);
+    }
+  }
+
+  useEffect(() => {
+    if (!Number.isFinite(customTrackingId)) {
+      setLoading(false);
+      setError("Invalid category id");
+      return;
+    }
+    refresh();
+    // eslint-disable-next-line react-hooks/exhaustive-deps
+  }, [customTrackingId]);
+
+  async function handleAddTask(name) {
+    const created = await createCustomCategoryTask(customTrackingId, { name });
+    setTasks((prev) => [created, ...(prev ?? [])]);
+  }
+
+  async function handleEditTask(taskId, name) {
+    const updated = await updateCustomCategoryTask(customTrackingId, taskId, { name });
+    setTasks((prev) => (prev ?? []).map((t) => (t.taskId === taskId ? updated : t)));
+  }
+
+  async function handleDeleteTask(taskId) {
+    await deleteCustomCategoryTask(customTrackingId, taskId);
+    setTasks((prev) => (prev ?? []).filter((t) => t.taskId !== taskId));
+  }
+
+  async function handleMoveTask(taskId, toFinished) {
+    // optimistic update
+    setTasks((prev) =>
+      (prev ?? []).map((t) =>
+        t.taskId === taskId ? { ...t, isFinished: toFinished } : t
+      )
+    );
+
+    try {
+      const saved = await updateCustomCategoryTaskFinished(
+        customTrackingId,
+        taskId,
+        toFinished
+      );
+      setTasks((prev) => (prev ?? []).map((t) => (t.taskId === taskId ? saved : t)));
+    } catch (e) {
+      // revert by refetching (simple + correct)
+      await refresh();
+      throw e;
+    }
+  }
+
+  return (
+    <div className="space-y-6">
+      <div className="flex flex-col gap-2 md:flex-row md:items-end md:justify-between">
+        <div>
+          <h1 className="text-2xl font-bold">{categoryName}</h1>
+          <p className="text-sm opacity-80">
+            Drag tickets between columns to mark them done / not done.
+          </p>
+        </div>
+      </div>
+
+      {error ? (
+        <div className="rounded-xl border border-red-500/30 bg-red-500/10 p-4 text-red-200">
+          {error}
+        </div>
+      ) : null}
+
+      <CustomCategoryKanbanBoard
+        loading={loading}
+        notDone={lanes.notDone}
+        done={lanes.done}
+        onAddTask={handleAddTask}
+        onEditTask={handleEditTask}
+        onDeleteTask={handleDeleteTask}
+        onMoveTask={handleMoveTask}
+      />
+    </div>
+  );
+}
+
+
Index: frontend/src/pages/Dashboard/pages/CustomCategory/components/CustomCategoryKanbanBoard.jsx
===================================================================
--- frontend/src/pages/Dashboard/pages/CustomCategory/components/CustomCategoryKanbanBoard.jsx	(revision e9a0543bbb622a2b343d1f5fe3b2df7ddb6a0bd3)
+++ frontend/src/pages/Dashboard/pages/CustomCategory/components/CustomCategoryKanbanBoard.jsx	(revision e9a0543bbb622a2b343d1f5fe3b2df7ddb6a0bd3)
@@ -0,0 +1,238 @@
+import React, { useMemo, useState } from "react";
+
+import {
+  DndContext,
+  PointerSensor,
+  closestCenter,
+  useSensor,
+  useSensors,
+} from "@dnd-kit/core";
+import {
+  SortableContext,
+  arrayMove,
+  rectSortingStrategy,
+} from "@dnd-kit/sortable";
+
+import KanbanColumn from "./KanbanColumn";
+import TaskTicketCard from "./TaskTicketCard";
+
+function laneIdForTask(task) {
+  return task?.isFinished ? "done" : "notDone";
+}
+
+export default function CustomCategoryKanbanBoard({
+  loading,
+  notDone,
+  done,
+  onAddTask,
+  onEditTask,
+  onDeleteTask,
+  onMoveTask,
+}) {
+  const [newName, setNewName] = useState("");
+  const [submitting, setSubmitting] = useState(false);
+
+  const sensors = useSensors(useSensor(PointerSensor, { activationConstraint: { distance: 6 } }));
+
+  // Keep a local order per lane for nicer UX, but still driven by server state.
+  const [notDoneOrder, setNotDoneOrder] = useState([]);
+  const [doneOrder, setDoneOrder] = useState([]);
+
+  const orderedNotDone = useMemo(() => {
+    const byId = new Map((notDone ?? []).map((t) => [String(t.taskId), t]));
+    const ids = [
+      ...notDoneOrder.filter((id) => byId.has(id)),
+      ...Array.from(byId.keys()).filter((id) => !notDoneOrder.includes(id)),
+    ];
+    return ids.map((id) => byId.get(id));
+  }, [notDone, notDoneOrder]);
+
+  const orderedDone = useMemo(() => {
+    const byId = new Map((done ?? []).map((t) => [String(t.taskId), t]));
+    const ids = [
+      ...doneOrder.filter((id) => byId.has(id)),
+      ...Array.from(byId.keys()).filter((id) => !doneOrder.includes(id)),
+    ];
+    return ids.map((id) => byId.get(id));
+  }, [done, doneOrder]);
+
+  async function submitNewTask(e) {
+    e.preventDefault();
+    const name = newName.trim();
+    if (!name || submitting) return;
+    setSubmitting(true);
+    try {
+      await onAddTask(name);
+      setNewName("");
+    } finally {
+      setSubmitting(false);
+    }
+  }
+
+  function findTaskById(id) {
+    const all = [...(notDone ?? []), ...(done ?? [])];
+    return all.find((t) => String(t.taskId) === String(id));
+  }
+
+  async function handleDragEnd(event) {
+    const { active, over } = event;
+    if (!over) return;
+
+    const activeId = String(active.id);
+    const overId = String(over.id);
+
+    if (activeId === overId) return;
+
+    const task = findTaskById(activeId);
+    if (!task) return;
+
+    // Moving over a column dropzone
+    if (overId === "lane:notDone" || overId === "lane:done") {
+      const toFinished = overId === "lane:done";
+      if (Boolean(task.isFinished) === toFinished) return;
+      await onMoveTask(task.taskId, toFinished);
+      return;
+    }
+
+    // Sorting within a lane: keep it frontend-only.
+    const overTask = findTaskById(overId);
+    if (!overTask) return;
+
+    const fromLane = laneIdForTask(task);
+    const toLane = laneIdForTask(overTask);
+
+    if (fromLane !== toLane) {
+      // if dropped onto a card in the other lane => treat as lane move
+      await onMoveTask(task.taskId, toLane === "done");
+      return;
+    }
+
+    if (fromLane === "notDone") {
+      const ids = orderedNotDone.map((t) => String(t.taskId));
+      const oldIndex = ids.indexOf(activeId);
+      const newIndex = ids.indexOf(overId);
+      if (oldIndex >= 0 && newIndex >= 0 && oldIndex !== newIndex) {
+        setNotDoneOrder(arrayMove(ids, oldIndex, newIndex));
+      }
+    } else {
+      const ids = orderedDone.map((t) => String(t.taskId));
+      const oldIndex = ids.indexOf(activeId);
+      const newIndex = ids.indexOf(overId);
+      if (oldIndex >= 0 && newIndex >= 0 && oldIndex !== newIndex) {
+        setDoneOrder(arrayMove(ids, oldIndex, newIndex));
+      }
+    }
+  }
+
+  return (
+    <div className="space-y-6">
+      <div className="rounded-xl border border-white/10 bg-neutral-900/40 p-4">
+        <div className="flex flex-col gap-3 md:flex-row md:items-center md:justify-between">
+          <div>
+            <div className="text-sm font-semibold text-white/90">
+              Tickets
+              <span className="ml-2 text-xs font-normal text-white/60">
+                Drag cards left/right to change status.
+              </span>
+            </div>
+            <div className="mt-1 text-xs text-white/55">
+              Tip: grab the <span className="font-semibold text-white/80">drag</span> handle.
+            </div>
+          </div>
+
+          <div className="flex items-center gap-3 text-xs text-white/70">
+            <span className="rounded-full bg-sky-500/15 px-2 py-1 text-sky-200">
+              Not done: {(notDone ?? []).length}
+            </span>
+            <span className="rounded-full bg-emerald-500/15 px-2 py-1 text-emerald-200">
+              Done: {(done ?? []).length}
+            </span>
+          </div>
+        </div>
+
+        <form onSubmit={submitNewTask} className="mt-4 flex flex-col gap-3 md:flex-row">
+          <input
+            value={newName}
+            onChange={(e) => setNewName(e.target.value)}
+            placeholder="Add a new ticket (e.g. Fix bug #123)"
+            maxLength={200}
+            className="w-full rounded-lg border border-white/10 bg-black/30 px-3 py-2 text-sm outline-none focus:border-green-400"
+          />
+          <button
+            className="rounded-lg bg-green-400 px-4 py-2 text-sm font-medium text-black disabled:opacity-60"
+            type="submit"
+            disabled={!newName.trim() || submitting}
+          >
+            {submitting ? "Adding..." : "Add"}
+          </button>
+        </form>
+      </div>
+
+      <DndContext
+        sensors={sensors}
+        collisionDetection={closestCenter}
+        onDragEnd={handleDragEnd}
+      >
+        <div className="grid grid-cols-1 gap-4 md:grid-cols-2">
+          <KanbanColumn
+            title="Not done"
+            droppableId="lane:notDone"
+            count={orderedNotDone.length}
+          >
+            <SortableContext
+              items={orderedNotDone.map((t) => String(t.taskId))}
+              strategy={rectSortingStrategy}
+            >
+              <div className="space-y-3">
+                {loading ? (
+                  <div className="text-sm opacity-70">Loading...</div>
+                ) : orderedNotDone.length === 0 ? (
+                  <div className="text-sm opacity-70">No tickets yet.</div>
+                ) : (
+                  orderedNotDone.map((t) => (
+                    <TaskTicketCard
+                      key={t.taskId}
+                      task={t}
+                      onEdit={(name) => onEditTask(t.taskId, name)}
+                      onDelete={() => onDeleteTask(t.taskId)}
+                    />
+                  ))
+                )}
+              </div>
+            </SortableContext>
+          </KanbanColumn>
+
+          <KanbanColumn
+            title="Done"
+            droppableId="lane:done"
+            count={orderedDone.length}
+          >
+            <SortableContext
+              items={orderedDone.map((t) => String(t.taskId))}
+              strategy={rectSortingStrategy}
+            >
+              <div className="space-y-3">
+                {loading ? (
+                  <div className="text-sm opacity-70">Loading...</div>
+                ) : orderedDone.length === 0 ? (
+                  <div className="text-sm opacity-70">Nothing done yet.</div>
+                ) : (
+                  orderedDone.map((t) => (
+                    <TaskTicketCard
+                      key={t.taskId}
+                      task={t}
+                      onEdit={(name) => onEditTask(t.taskId, name)}
+                      onDelete={() => onDeleteTask(t.taskId)}
+                    />
+                  ))
+                )}
+              </div>
+            </SortableContext>
+          </KanbanColumn>
+        </div>
+      </DndContext>
+    </div>
+  );
+}
+
+
Index: frontend/src/pages/Dashboard/pages/CustomCategory/components/KanbanColumn.jsx
===================================================================
--- frontend/src/pages/Dashboard/pages/CustomCategory/components/KanbanColumn.jsx	(revision e9a0543bbb622a2b343d1f5fe3b2df7ddb6a0bd3)
+++ frontend/src/pages/Dashboard/pages/CustomCategory/components/KanbanColumn.jsx	(revision e9a0543bbb622a2b343d1f5fe3b2df7ddb6a0bd3)
@@ -0,0 +1,70 @@
+import React, { useMemo } from "react";
+import { useDroppable } from "@dnd-kit/core";
+import { MdCheckCircle, MdRadioButtonUnchecked } from "react-icons/md";
+
+export default function KanbanColumn({ title, droppableId, count, children }) {
+  const { setNodeRef, isOver } = useDroppable({ id: droppableId });
+
+  const variant = useMemo(() => {
+    if (droppableId === "lane:done") {
+      return {
+        Icon: MdCheckCircle,
+        header: "from-emerald-500/25 via-emerald-500/10 to-transparent",
+        badge: "bg-emerald-500/15 text-emerald-200 border-emerald-500/30",
+        ring: "group-hover:ring-emerald-500/25",
+        drop: "border-emerald-400/60 bg-emerald-400/10",
+        border: "border-emerald-500/20",
+      };
+    }
+
+    return {
+      Icon: MdRadioButtonUnchecked,
+      header: "from-sky-500/25 via-sky-500/10 to-transparent",
+      badge: "bg-sky-500/15 text-sky-200 border-sky-500/30",
+      ring: "group-hover:ring-sky-500/25",
+      drop: "border-sky-400/60 bg-sky-400/10",
+      border: "border-sky-500/20",
+    };
+  }, [droppableId]);
+
+  return (
+    <div
+      className={
+        "group overflow-hidden rounded-xl border bg-neutral-900/40 shadow-sm ring-1 ring-white/5 transition " +
+        variant.border +
+        " " +
+        variant.ring
+      }
+    >
+      <div className={"bg-gradient-to-b " + variant.header + " p-4"}>
+        <div className="flex items-center justify-between gap-3">
+          <div className="flex items-center gap-2">
+            <variant.Icon className="size-5 text-white/80" />
+            <h2 className="text-sm font-semibold tracking-wide text-white/90">
+              {title}
+            </h2>
+          </div>
+          <span
+            className={
+              "rounded-full border px-2 py-0.5 text-xs font-medium " +
+              variant.badge
+            }
+          >
+            {count}
+          </span>
+        </div>
+      </div>
+
+      <div
+        ref={setNodeRef}
+        className={
+          "min-h-[220px] p-4 transition-colors " +
+          (isOver ? variant.drop : "border-t border-white/10")
+        }
+      >
+        {children}
+      </div>
+    </div>
+  );
+}
+
Index: frontend/src/pages/Dashboard/pages/CustomCategory/components/TaskTicketCard.jsx
===================================================================
--- frontend/src/pages/Dashboard/pages/CustomCategory/components/TaskTicketCard.jsx	(revision e9a0543bbb622a2b343d1f5fe3b2df7ddb6a0bd3)
+++ frontend/src/pages/Dashboard/pages/CustomCategory/components/TaskTicketCard.jsx	(revision e9a0543bbb622a2b343d1f5fe3b2df7ddb6a0bd3)
@@ -0,0 +1,120 @@
+import React, { useMemo, useState } from "react";
+import { useSortable } from "@dnd-kit/sortable";
+import { CSS } from "@dnd-kit/utilities";
+import { MdDelete, MdDragIndicator, MdEdit, MdSave } from "react-icons/md";
+
+export default function TaskTicketCard({ task, onEdit, onDelete }) {
+  const id = String(task.taskId);
+  const {
+    attributes,
+    listeners,
+    setNodeRef,
+    transform,
+    transition,
+    isDragging,
+  } = useSortable({ id });
+
+  const style = useMemo(
+    () => ({
+      transform: CSS.Transform.toString(transform),
+      transition,
+    }),
+    [transform, transition]
+  );
+
+  const [editing, setEditing] = useState(false);
+  const [draft, setDraft] = useState(task.name ?? "");
+
+  async function submitEdit(e) {
+    e.preventDefault();
+    const next = draft.trim();
+    if (!next) return;
+    await onEdit(next);
+    setEditing(false);
+  }
+
+  return (
+    <div
+      ref={setNodeRef}
+      style={style}
+      className={
+        "group rounded-lg border border-white/10 bg-black/30 p-3 shadow-sm transition hover:border-white/20 hover:bg-black/35 " +
+        (task.isFinished ? "opacity-90" : "") +
+        (isDragging ? " opacity-60" : "")
+      }
+    >
+      <div className="flex items-start justify-between gap-3">
+        <div className="min-w-0 flex-1">
+          {editing ? (
+            <form onSubmit={submitEdit} className="flex gap-2">
+              <input
+                value={draft}
+                onChange={(e) => setDraft(e.target.value)}
+                maxLength={200}
+                className="w-full rounded-md border border-white/10 bg-black/40 px-2 py-1 text-sm outline-none focus:border-green-400"
+                autoFocus
+              />
+              <button
+                type="submit"
+                className="inline-flex items-center gap-1 rounded-md bg-green-400 px-2 py-1 text-sm font-medium text-black"
+              >
+                <MdSave className="size-4" />
+                Save
+              </button>
+            </form>
+          ) : (
+            <div className="flex items-center gap-2">
+              <span
+                className={
+                  "h-2 w-2 shrink-0 rounded-full " +
+                  (task.isFinished ? "bg-emerald-400" : "bg-sky-400")
+                }
+              />
+              <div className="min-w-0 text-sm font-medium text-white/90">
+                {task.name}
+              </div>
+            </div>
+          )}
+
+          <div className="mt-1 text-xs text-white/50">Ticket #{task.taskId}</div>
+        </div>
+
+        <div className="flex shrink-0 items-center gap-2">
+          {!editing ? (
+            <button
+              type="button"
+              onClick={() => {
+                setDraft(task.name ?? "");
+                setEditing(true);
+              }}
+              className="rounded-md p-1 text-white/70 hover:bg-white/10 hover:text-white"
+              title="Edit"
+            >
+              <MdEdit className="size-5" />
+            </button>
+          ) : null}
+
+          <button
+            type="button"
+            onClick={onDelete}
+            className="rounded-md p-1 text-white/70 hover:bg-white/10 hover:text-white"
+            title="Delete"
+          >
+            <MdDelete className="size-5" />
+          </button>
+
+          <button
+            type="button"
+            className="inline-flex cursor-grab items-center gap-1 rounded-md p-1 text-white/70 hover:bg-white/10 hover:text-white active:cursor-grabbing"
+            title="Drag"
+            {...attributes}
+            {...listeners}
+          >
+            <MdDragIndicator className="size-5" />
+          </button>
+        </div>
+      </div>
+    </div>
+  );
+}
+
