Index: backend/src/main/java/com/trekr/backend/controller/CustomTrackingController.java
===================================================================
--- backend/src/main/java/com/trekr/backend/controller/CustomTrackingController.java	(revision e9a0543bbb622a2b343d1f5fe3b2df7ddb6a0bd3)
+++ backend/src/main/java/com/trekr/backend/controller/CustomTrackingController.java	(revision e9a0543bbb622a2b343d1f5fe3b2df7ddb6a0bd3)
@@ -0,0 +1,91 @@
+package com.trekr.backend.controller;
+
+import com.trekr.backend.dto.discipline.CreateCustomTrackingCategoryRequest;
+import com.trekr.backend.dto.discipline.CreateTaskRequest;
+import com.trekr.backend.dto.discipline.CustomTrackingCategoriesResponse;
+import com.trekr.backend.dto.discipline.CustomTrackingCategoryDto;
+import com.trekr.backend.dto.discipline.TaskDto;
+import com.trekr.backend.dto.discipline.TasksResponse;
+import com.trekr.backend.dto.discipline.UpdateTaskFinishedRequest;
+import com.trekr.backend.dto.discipline.UpdateTaskRequest;
+import com.trekr.backend.security.UserPrincipal;
+import com.trekr.backend.service.CustomTrackingService;
+import jakarta.validation.Valid;
+import org.springframework.http.HttpStatus;
+import org.springframework.security.core.Authentication;
+import org.springframework.web.bind.annotation.*;
+
+@RestController
+@RequestMapping("/api/discipline/custom-tracking-categories")
+public class CustomTrackingController {
+
+    private final CustomTrackingService customTrackingService;
+
+    public CustomTrackingController(CustomTrackingService customTrackingService) {
+        this.customTrackingService = customTrackingService;
+    }
+
+    @GetMapping
+    public CustomTrackingCategoriesResponse customTrackingCategories(Authentication authentication) {
+        UserPrincipal principal = (UserPrincipal) authentication.getPrincipal();
+        return customTrackingService.getCustomTrackingCategories(principal.getUserId());
+    }
+
+    @PostMapping
+    @ResponseStatus(HttpStatus.CREATED)
+    public CustomTrackingCategoryDto createCustomTrackingCategory(
+            @Valid @RequestBody CreateCustomTrackingCategoryRequest request,
+            Authentication authentication) {
+        UserPrincipal principal = (UserPrincipal) authentication.getPrincipal();
+        return customTrackingService.createCustomTrackingCategory(principal.getUserId(), request);
+    }
+
+    @GetMapping("/{customTrackingId}/tasks")
+    public TasksResponse tasks(
+            @PathVariable Long customTrackingId,
+            Authentication authentication) {
+        UserPrincipal principal = (UserPrincipal) authentication.getPrincipal();
+        return customTrackingService.getCustomCategoryTasks(principal.getUserId(), customTrackingId);
+    }
+
+    @PostMapping("/{customTrackingId}/tasks")
+    @ResponseStatus(HttpStatus.CREATED)
+    public TaskDto createTask(
+            @PathVariable Long customTrackingId,
+            @Valid @RequestBody CreateTaskRequest request,
+            Authentication authentication) {
+        UserPrincipal principal = (UserPrincipal) authentication.getPrincipal();
+        return customTrackingService.createCustomCategoryTask(principal.getUserId(), customTrackingId, request);
+    }
+
+    @PutMapping("/{customTrackingId}/tasks/{taskId}")
+    public TaskDto updateTask(
+            @PathVariable Long customTrackingId,
+            @PathVariable Long taskId,
+            @Valid @RequestBody UpdateTaskRequest request,
+            Authentication authentication) {
+        UserPrincipal principal = (UserPrincipal) authentication.getPrincipal();
+        return customTrackingService.updateCustomCategoryTask(principal.getUserId(), customTrackingId, taskId, request);
+    }
+
+    @PatchMapping("/{customTrackingId}/tasks/{taskId}/finished")
+    public TaskDto updateTaskFinished(
+            @PathVariable Long customTrackingId,
+            @PathVariable Long taskId,
+            @Valid @RequestBody UpdateTaskFinishedRequest request,
+            Authentication authentication) {
+        UserPrincipal principal = (UserPrincipal) authentication.getPrincipal();
+        return customTrackingService.updateCustomCategoryTaskFinished(principal.getUserId(), customTrackingId, taskId, request);
+    }
+
+    @DeleteMapping("/{customTrackingId}/tasks/{taskId}")
+    @ResponseStatus(HttpStatus.NO_CONTENT)
+    public void deleteTask(
+            @PathVariable Long customTrackingId,
+            @PathVariable Long taskId,
+            Authentication authentication) {
+        UserPrincipal principal = (UserPrincipal) authentication.getPrincipal();
+        customTrackingService.deleteCustomCategoryTask(principal.getUserId(), customTrackingId, taskId);
+    }
+}
+
Index: backend/src/main/java/com/trekr/backend/dto/discipline/CreateCustomTrackingCategoryRequest.java
===================================================================
--- backend/src/main/java/com/trekr/backend/dto/discipline/CreateCustomTrackingCategoryRequest.java	(revision e9a0543bbb622a2b343d1f5fe3b2df7ddb6a0bd3)
+++ backend/src/main/java/com/trekr/backend/dto/discipline/CreateCustomTrackingCategoryRequest.java	(revision e9a0543bbb622a2b343d1f5fe3b2df7ddb6a0bd3)
@@ -0,0 +1,29 @@
+
+package com.trekr.backend.dto.discipline;
+
+import jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.Size;
+
+public class CreateCustomTrackingCategoryRequest {
+
+	@NotBlank(message = "Name is required")
+	@Size(max = 100, message = "Name must be at most 100 characters")
+	private String name;
+
+	public CreateCustomTrackingCategoryRequest() {
+	}
+
+	public CreateCustomTrackingCategoryRequest(String name) {
+		this.name = name;
+	}
+
+	public String getName() {
+		return name;
+	}
+
+	public void setName(String name) {
+		this.name = name;
+	}
+}
+
+
Index: backend/src/main/java/com/trekr/backend/dto/discipline/CustomTrackingCategoriesResponse.java
===================================================================
--- backend/src/main/java/com/trekr/backend/dto/discipline/CustomTrackingCategoriesResponse.java	(revision e9a0543bbb622a2b343d1f5fe3b2df7ddb6a0bd3)
+++ backend/src/main/java/com/trekr/backend/dto/discipline/CustomTrackingCategoriesResponse.java	(revision e9a0543bbb622a2b343d1f5fe3b2df7ddb6a0bd3)
@@ -0,0 +1,26 @@
+
+package com.trekr.backend.dto.discipline;
+
+import java.util.List;
+
+public class CustomTrackingCategoriesResponse {
+
+	private List<CustomTrackingCategoryDto> items;
+
+	public CustomTrackingCategoriesResponse() {
+	}
+
+	public CustomTrackingCategoriesResponse(List<CustomTrackingCategoryDto> items) {
+		this.items = items;
+	}
+
+	public List<CustomTrackingCategoryDto> getItems() {
+		return items;
+	}
+
+	public void setItems(List<CustomTrackingCategoryDto> items) {
+		this.items = items;
+	}
+}
+
+
Index: backend/src/main/java/com/trekr/backend/dto/discipline/CustomTrackingCategoryDto.java
===================================================================
--- backend/src/main/java/com/trekr/backend/dto/discipline/CustomTrackingCategoryDto.java	(revision e9a0543bbb622a2b343d1f5fe3b2df7ddb6a0bd3)
+++ backend/src/main/java/com/trekr/backend/dto/discipline/CustomTrackingCategoryDto.java	(revision e9a0543bbb622a2b343d1f5fe3b2df7ddb6a0bd3)
@@ -0,0 +1,34 @@
+
+package com.trekr.backend.dto.discipline;
+
+public class CustomTrackingCategoryDto {
+
+	private Long customTrackingId;
+	private String name;
+
+	public CustomTrackingCategoryDto() {
+	}
+
+	public CustomTrackingCategoryDto(Long customTrackingId, String name) {
+		this.customTrackingId = customTrackingId;
+		this.name = name;
+	}
+
+	public Long getCustomTrackingId() {
+		return customTrackingId;
+	}
+
+	public void setCustomTrackingId(Long customTrackingId) {
+		this.customTrackingId = customTrackingId;
+	}
+
+	public String getName() {
+		return name;
+	}
+
+	public void setName(String name) {
+		this.name = name;
+	}
+}
+
+
Index: backend/src/main/java/com/trekr/backend/repository/CustomTrackingCategoryRepository.java
===================================================================
--- backend/src/main/java/com/trekr/backend/repository/CustomTrackingCategoryRepository.java	(revision e9a0543bbb622a2b343d1f5fe3b2df7ddb6a0bd3)
+++ backend/src/main/java/com/trekr/backend/repository/CustomTrackingCategoryRepository.java	(revision e9a0543bbb622a2b343d1f5fe3b2df7ddb6a0bd3)
@@ -0,0 +1,16 @@
+
+package com.trekr.backend.repository;
+
+import com.trekr.backend.entity.discipline.CustomTrackingCategory;
+import org.springframework.data.jpa.repository.JpaRepository;
+
+import java.util.List;
+
+public interface CustomTrackingCategoryRepository extends JpaRepository<CustomTrackingCategory, Long> {
+
+	List<CustomTrackingCategory> findByUser_UserIdOrderByCustomTrackingIdDesc(Long userId);
+
+	boolean existsByUser_UserIdAndNameIgnoreCase(Long userId, String name);
+}
+
+
Index: backend/src/main/java/com/trekr/backend/repository/TaskRepository.java
===================================================================
--- backend/src/main/java/com/trekr/backend/repository/TaskRepository.java	(revision 4f85b41e4eaa6d668912791fb3cff6efbe7dc1a4)
+++ backend/src/main/java/com/trekr/backend/repository/TaskRepository.java	(revision e9a0543bbb622a2b343d1f5fe3b2df7ddb6a0bd3)
@@ -23,4 +23,10 @@
     List<Task> findByDisciplineUser_UserIdAndFinishedTrue(Long userId);
 
+    List<Task> findByCustomTrackingCategory_CustomTrackingIdAndCustomTrackingCategory_User_UserIdOrderByTaskIdDesc(
+            Long customTrackingId, Long userId);
+
+    int deleteByTaskIdAndCustomTrackingCategory_CustomTrackingIdAndCustomTrackingCategory_User_UserId(
+            Long taskId, Long customTrackingId, Long userId);
+
     @Modifying
     @Query("update Task t set t.finished = false where t.disciplineUser.userId = :userId")
Index: backend/src/main/java/com/trekr/backend/service/CustomTrackingService.java
===================================================================
--- backend/src/main/java/com/trekr/backend/service/CustomTrackingService.java	(revision e9a0543bbb622a2b343d1f5fe3b2df7ddb6a0bd3)
+++ backend/src/main/java/com/trekr/backend/service/CustomTrackingService.java	(revision e9a0543bbb622a2b343d1f5fe3b2df7ddb6a0bd3)
@@ -0,0 +1,183 @@
+package com.trekr.backend.service;
+
+import com.trekr.backend.dto.discipline.CreateCustomTrackingCategoryRequest;
+import com.trekr.backend.dto.discipline.CreateTaskRequest;
+import com.trekr.backend.dto.discipline.CustomTrackingCategoriesResponse;
+import com.trekr.backend.dto.discipline.CustomTrackingCategoryDto;
+import com.trekr.backend.dto.discipline.TaskDto;
+import com.trekr.backend.dto.discipline.TasksResponse;
+import com.trekr.backend.dto.discipline.UpdateTaskFinishedRequest;
+import com.trekr.backend.dto.discipline.UpdateTaskRequest;
+import com.trekr.backend.entity.User;
+import com.trekr.backend.entity.discipline.CustomTrackingCategory;
+import com.trekr.backend.entity.discipline.Task;
+import com.trekr.backend.repository.CustomTrackingCategoryRepository;
+import com.trekr.backend.repository.TaskRepository;
+import com.trekr.backend.repository.UserRepository;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.util.List;
+
+@Service
+public class CustomTrackingService {
+
+    private final CustomTrackingCategoryRepository customTrackingCategoryRepository;
+    private final TaskRepository taskRepository;
+    private final UserRepository userRepository;
+
+    public CustomTrackingService(
+            CustomTrackingCategoryRepository customTrackingCategoryRepository,
+            TaskRepository taskRepository,
+            UserRepository userRepository) {
+        this.customTrackingCategoryRepository = customTrackingCategoryRepository;
+        this.taskRepository = taskRepository;
+        this.userRepository = userRepository;
+    }
+
+    public CustomTrackingCategoriesResponse getCustomTrackingCategories(Long userId) {
+        List<CustomTrackingCategoryDto> items = customTrackingCategoryRepository
+                .findByUser_UserIdOrderByCustomTrackingIdDesc(userId)
+                .stream()
+                .map(c -> new CustomTrackingCategoryDto(c.getCustomTrackingId(), c.getName()))
+                .toList();
+
+        return new CustomTrackingCategoriesResponse(items);
+    }
+
+    @Transactional
+    public CustomTrackingCategoryDto createCustomTrackingCategory(Long userId, CreateCustomTrackingCategoryRequest request) {
+        String name = request.getName();
+        if (name == null || name.isBlank()) {
+            throw new RuntimeException("Name is required");
+        }
+        String trimmed = name.trim();
+        if (trimmed.length() > 100) {
+            throw new RuntimeException("Name is too long");
+        }
+
+        if (customTrackingCategoryRepository.existsByUser_UserIdAndNameIgnoreCase(userId, trimmed)) {
+            throw new RuntimeException("Custom category with that name already exists");
+        }
+
+        User user = userRepository.findById(userId)
+                .orElseThrow(() -> new RuntimeException("User not found"));
+
+        CustomTrackingCategory category = new CustomTrackingCategory();
+        category.setUser(user);
+        category.setName(trimmed);
+        CustomTrackingCategory saved = customTrackingCategoryRepository.save(category);
+
+        return new CustomTrackingCategoryDto(saved.getCustomTrackingId(), saved.getName());
+    }
+
+    public TasksResponse getCustomCategoryTasks(Long userId, Long customTrackingId) {
+        requireOwnership(userId, customTrackingId);
+
+        List<TaskDto> tasks = taskRepository
+                .findByCustomTrackingCategory_CustomTrackingIdAndCustomTrackingCategory_User_UserIdOrderByTaskIdDesc(
+                        customTrackingId, userId)
+                .stream()
+                .map(t -> new TaskDto(t.getTaskId(), t.getName(), t.isFinished()))
+                .toList();
+
+        return new TasksResponse(tasks, false);
+    }
+
+    @Transactional
+    public TaskDto createCustomCategoryTask(Long userId, Long customTrackingId, CreateTaskRequest request) {
+        CustomTrackingCategory category = requireOwnership(userId, customTrackingId);
+
+        String name = request.getName();
+        if (name == null || name.isBlank()) {
+            throw new RuntimeException("Task name is required");
+        }
+        if (name.length() > 200) {
+            throw new RuntimeException("Task name is too long");
+        }
+
+        Task task = new Task();
+        task.setName(name.trim());
+        task.setFinished(false);
+        task.setDisciplineUser(null);
+        task.setCustomTrackingCategory(category);
+
+        Task saved = taskRepository.save(task);
+        return new TaskDto(saved.getTaskId(), saved.getName(), saved.isFinished());
+    }
+
+    @Transactional
+    public TaskDto updateCustomCategoryTask(Long userId, Long customTrackingId, Long taskId, UpdateTaskRequest request) {
+        Task task = taskRepository.findById(taskId)
+                .orElseThrow(() -> new RuntimeException("Task not found"));
+
+        if (task.getCustomTrackingCategory() == null
+                || !customTrackingId.equals(task.getCustomTrackingCategory().getCustomTrackingId())
+                || task.getCustomTrackingCategory().getUser() == null
+                || !userId.equals(task.getCustomTrackingCategory().getUser().getUserId())) {
+            throw new RuntimeException("Task not found");
+        }
+
+        String name = request.getName();
+        if (name == null || name.isBlank()) {
+            throw new RuntimeException("Task name is required");
+        }
+
+        task.setName(name.trim());
+        Task saved = taskRepository.save(task);
+        return new TaskDto(saved.getTaskId(), saved.getName(), saved.isFinished());
+    }
+
+    @Transactional
+    public TaskDto updateCustomCategoryTaskFinished(Long userId, Long customTrackingId, Long taskId,
+                                                    UpdateTaskFinishedRequest request) {
+        Task task = taskRepository.findById(taskId)
+                .orElseThrow(() -> new RuntimeException("Task not found"));
+
+        if (task.getCustomTrackingCategory() == null
+                || !customTrackingId.equals(task.getCustomTrackingCategory().getCustomTrackingId())
+                || task.getCustomTrackingCategory().getUser() == null
+                || !userId.equals(task.getCustomTrackingCategory().getUser().getUserId())) {
+            throw new RuntimeException("Task not found");
+        }
+
+        Boolean finished = request.getIsFinished();
+        if (finished == null) {
+            throw new RuntimeException("isFinished is required");
+        }
+
+        task.setFinished(finished);
+        Task saved = taskRepository.save(task);
+        return new TaskDto(saved.getTaskId(), saved.getName(), saved.isFinished());
+    }
+
+    @Transactional
+    public void deleteCustomCategoryTask(Long userId, Long customTrackingId, Long taskId) {
+        if (taskId == null || customTrackingId == null) {
+            throw new RuntimeException("Task not found");
+        }
+
+        int deleted = taskRepository
+                .deleteByTaskIdAndCustomTrackingCategory_CustomTrackingIdAndCustomTrackingCategory_User_UserId(
+                        taskId, customTrackingId, userId);
+        if (deleted == 0) {
+            throw new RuntimeException("Task not found");
+        }
+    }
+
+    private CustomTrackingCategory requireOwnership(Long userId, Long customTrackingId) {
+        if (customTrackingId == null) {
+            throw new RuntimeException("Custom tracking category not found");
+        }
+
+        CustomTrackingCategory category = customTrackingCategoryRepository.findById(customTrackingId)
+                .orElseThrow(() -> new RuntimeException("Custom tracking category not found"));
+
+        if (category.getUser() == null || !userId.equals(category.getUser().getUserId())) {
+            throw new RuntimeException("Custom tracking category not found");
+        }
+
+        return category;
+    }
+}
+
Index: backend/src/main/java/com/trekr/backend/service/DisciplineService.java
===================================================================
--- backend/src/main/java/com/trekr/backend/service/DisciplineService.java	(revision 4f85b41e4eaa6d668912791fb3cff6efbe7dc1a4)
+++ backend/src/main/java/com/trekr/backend/service/DisciplineService.java	(revision e9a0543bbb622a2b343d1f5fe3b2df7ddb6a0bd3)
@@ -52,4 +52,5 @@
     }
 
+
     public boolean isTracking(Long userId) {
         return disciplineUserRepository.existsById(userId);
Index: backend/src/main/resources/application.properties
===================================================================
--- backend/src/main/resources/application.properties	(revision 4f85b41e4eaa6d668912791fb3cff6efbe7dc1a4)
+++ backend/src/main/resources/application.properties	(revision e9a0543bbb622a2b343d1f5fe3b2df7ddb6a0bd3)
@@ -42,6 +42,8 @@
 # ============================
 spring.datasource.hikari.pool-name=trekrHikariPool
-spring.datasource.hikari.maximum-pool-size=20
-spring.datasource.hikari.minimum-idle=5
+# Remote PostgreSQL (especially via SSH tunnel) often has a small max_connections.
+# Use conservative defaults to avoid exhausting connection slots; override via env if needed.
+spring.datasource.hikari.maximum-pool-size=${SPRING_DATASOURCE_HIKARI_MAX_POOL_SIZE:4}
+spring.datasource.hikari.minimum-idle=${SPRING_DATASOURCE_HIKARI_MIN_IDLE:0}
 spring.datasource.hikari.connection-timeout=30000
 spring.datasource.hikari.idle-timeout=600000
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>
+  );
+}
+
