Skip to main content
Back to examples
Popular features/Split tasks
Loading live demo…from dhtmlxcode.com
Popular features

Split tasks

Cut a task into segments with a scissors tool where work pauses, keeping the pieces on one row with a labeled gap between them.

gantt.plugins({ tooltip: true });

gantt.config.date_format = "%Y-%m-%d";
gantt.config.work_time   = true;
gantt.config.row_height   = 36;
gantt.config.bar_height   = 24;
gantt.config.scale_height = 52;
gantt.config.min_column_width = 34;
gantt.config.drag_links = false;          // no link handles: this demo is about splitting, not linking
gantt.config.open_split_tasks = true;


/* ---------------------------------------------------------------------------
 *  Anchor the sprint to next Monday so the demo never looks stale.
 * ------------------------------------------------------------------------ */
function mondayOnOrAfter(date) {
	date = gantt.date.day_start(new Date(date));
	while (date.getDay() !== 1) date = gantt.date.add(date, 1, "day");
	return date;
}
let PROJECT_START = mondayOnOrAfter(new Date());
function wd(n) { return gantt.date.add(PROJECT_START, n, "day"); }

let PAUSE_DAYS = 4;   // how far a cut pushes the second session out

gantt.config.fit_tasks = true;

gantt.config.scales = [
	{ unit: "month", step: 1, format: "%F %Y" },
	{ unit: "day",   step: 1, format: "%j %D" }
];

gantt.templates.scale_cell_class    = function (date)       { return gantt.isWorkTime(date) ? "" : "weekend"; };
gantt.templates.timeline_cell_class = function (task, date) { return gantt.isWorkTime({ date, task }) ? "" : "weekend"; };

/* A split parent carries its own centred label that would ghost through the
   gaps, so it stays blank. Every child segment gets a chronological number,
   while regular tasks keep their normal text. */
function isSplit(task) { return task.render === "split"; }

function getSplitParent(task) {
	let parentId = gantt.getParent(task.id);
	if (!parentId || !gantt.isTaskExists(parentId)) return null;
	let parent = gantt.getTask(parentId);
	return isSplit(parent) ? parent : null;
}

function getOrderedSegments(parentId) {
	return gantt.getChildren(parentId)
		.map(function (id) { return gantt.getTask(id); })
		.sort(function (a, b) {
			let byStart = a.start_date - b.start_date;
			return byStart || (a.end_date - b.end_date);
		});
}

function getSegmentNumber(task) {
	let parent = getSplitParent(task);
	if (!parent) return 0;
	let segments = getOrderedSegments(parent.id);
	for (let i = 0; i < segments.length; i++) {
		if (segments[i].id == task.id) return i + 1;
	}
	return 0;
}

gantt.templates.task_text = function (start, end, task) {
	if (isSplit(task)) return "";
	let segmentNumber = getSegmentNumber(task);
	return segmentNumber ? "#" + segmentNumber : task.text;
};

gantt.templates.grid_row_class = function (start, end, task) { return isSplit(task) ? "split_row" : ""; };

/* ---------------------------------------------------------------------------
 *  Pause reasons -- the picker drives the label a new interruption gets.
 * ------------------------------------------------------------------------ */
let REASONS = [
	{ key: "vacation", label: "Vacation" },
	{ key: "oncall",   label: "On-call" },
	{ key: "hotfix",   label: "Hotfix" },
	{ key: "training", label: "Training" },
	{ key: "leave",    label: "Parental leave" },
	{ key: "break",    label: "Sprint break" }
];
let CAUSE_LABELS = {};
REASONS.forEach(function (r) { CAUSE_LABELS[r.key] = r.label; });
let currentReason = "oncall";

function buildReasonMenu() {
	document.getElementById("reason_menu").innerHTML = REASONS.map(function (r) {
		return '<div class="dhx_menu__item dhx_menu__item--check' + (r.key === currentReason ? " checked" : "") +
			'" onclick="pickReason(\'' + r.key + '\')">' + r.label + '</div>';
	}).join("");
}
function pickReason(key) {
	currentReason = key;
	DHX.ui.setText("reason_label", CAUSE_LABELS[key]);
	buildReasonMenu();
	DHX.dd.closeAll();
}

/* ---------------------------------------------------------------------------
 *  GRID
 * ------------------------------------------------------------------------ */
function ownerCell(task) { return task.owner ? DHX.ui.escape(task.owner) : ""; }

function pausedCell(task) {
	if (!isSplit(task)) return "";
	let key = task.cause || "break";
	return '<span class="cause_chip cause_' + key + '"><span class="cause_dot"></span>' + (CAUSE_LABELS[key] || "Paused") + '</span>';
}

gantt.config.columns = [
	{ name: "text",   label: "Task",       tree: true, width: 230, resize: true, editor: { type: "text", map_to: "text" } },
	{ name: "owner",  label: "Owner",      align: "left",   width: 130, resize: true, template: ownerCell },
	{ name: "paused", label: "Paused for", align: "center", width: 132, resize: true, template: pausedCell },
	{ name: "add", width: 40, resize: true }
];

let dateToStr = gantt.date.date_to_str("%d %M");
gantt.templates.tooltip_text = function (start, end, task) {
	let splitParent = getSplitParent(task);
	let segmentNumber = splitParent ? getSegmentNumber(task) : 0;
	let name = splitParent ? splitParent.text + " (#" + segmentNumber + ")" : task.text;
	let html = "<b>" + DHX.ui.escape(name || "") + "</b>";
	if (task.owner) html += "<br>" + DHX.ui.escape(task.owner);
	if (isSplit(task) && task.cause) html += "<br>Paused for " + (CAUSE_LABELS[task.cause] || "");
	html += "<br>" + dateToStr(start) + " - " + dateToStr(end);
	return html;
};

/* ---------------------------------------------------------------------------
 *  DATA: two sprint swimlanes. Two tasks start out interrupted so the pause
 *  pill is visible on load; the rest are continuous, waiting for the scissors.
 * ------------------------------------------------------------------------ */
function buildData() {
	return {
		tasks: [
			{ id: 1, text: "Sprint 24: Checkout & Payments Overhaul", type: "project", open: true },

			{ id: 10, text: "Sprint planning & backlog grooming", parent: 1, start_date: wd(1), duration: 1, progress: 1, owner: "Whole team" },

			/* pre-interrupted: two sessions around a vacation */
			{ id: 20, text: "Search filters redesign", parent: 1, type: "project", render: "split", open: false, cause: "vacation", owner: "Priya Shah" },
			{ id: 21, text: "Search filters redesign", parent: 20, start_date: wd(1),  duration: 3, progress: 0.4, owner: "Priya Shah" },
			{ id: 22, text: "",                         parent: 20, start_date: wd(11), duration: 4, progress: 0,   owner: "Priya Shah" },

			{ id: 40, text: "Checkout API refactor",   parent: 1, start_date: wd(2),  duration: 8, progress: 0.25, owner: "Elena Volkov" },
			{ id: 44, text: "Coupon engine rework",    parent: 1, start_date: wd(4),  duration: 6, progress: 0.1,  owner: "Priya Shah" },
			{ id: 45, text: "Cart persistence bugfix", parent: 1, start_date: wd(9),  duration: 4, progress: 0,    owner: "Noah Kim" },
			{ id: 62, text: "Address autocomplete",    parent: 1, start_date: wd(12), duration: 5, progress: 0,    owner: "Noah Kim" },
			{ id: 60, text: "Update API documentation",parent: 1, start_date: wd(16), duration: 3, progress: 0,    owner: "Jordan Lee" },

			{ id: 2, text: "Sprint 24: Trust & Safety", type: "project", open: true },

			/* pre-interrupted: two sessions around a training week */
			{ id: 50, text: "Fraud detection rules engine", parent: 2, type: "project", render: "split", open: false, cause: "training", owner: "Sam Okafor" },
			{ id: 51, text: "Fraud detection rules engine", parent: 50, start_date: wd(7),  duration: 3, progress: 0.35, owner: "Sam Okafor" },
			{ id: 52, text: "",                             parent: 50, start_date: wd(16), duration: 4, progress: 0,    owner: "Sam Okafor" },

			{ id: 30, text: "Payment gateway integration", parent: 2, start_date: wd(3),  duration: 7, progress: 0.2, owner: "Marcus Webb" },
			{ id: 33, text: "Bot mitigation tuning",       parent: 2, start_date: wd(6),  duration: 6, progress: 0,   owner: "Sam Okafor" },
			{ id: 36, text: "Audit log pipeline",          parent: 2, start_date: wd(13), duration: 5, progress: 0,   owner: "Dana Ruiz" },
			{ id: 70, text: "Regression test pass",        parent: 2, start_date: wd(22), duration: 4, progress: 0,   owner: "QA Guild" },
			{ id: 95, text: "Security review sign-off",    parent: 2, start_date: wd(25), duration: 2, progress: 0,   owner: "Security Guild" },
			{ id: 99, text: "Sprint 24 complete", parent: 2, type: "milestone", start_date: wd(30), duration: 0, progress: 0 },

			{ id: 3, text: "Sprint 24: Platform & Infra", type: "project", open: true },

			{ id: 80, text: "Infra capacity planning", parent: 3, start_date: wd(1), duration: 3, progress: 0.5, owner: "Dana Ruiz" },

			/* pre-interrupted: two sessions around a hotfix */
			{ id: 85, text: "CI pipeline upgrade", parent: 3, type: "project", render: "split", open: false, cause: "hotfix", owner: "Elena Volkov" },
			{ id: 86, text: "CI pipeline upgrade", parent: 85, start_date: wd(3),  duration: 3, progress: 0.4, owner: "Elena Volkov" },
			{ id: 87, text: "",                    parent: 85, start_date: wd(10), duration: 3, progress: 0,   owner: "Elena Volkov" },

			{ id: 90, text: "Database migration dry-run",   parent: 3, start_date: wd(7),  duration: 6, progress: 0, owner: "Marcus Webb" },
			{ id: 91, text: "Observability dashboards",     parent: 3, start_date: wd(9),  duration: 4, progress: 0, owner: "Jordan Lee" },
			{ id: 92, text: "Load testing",                 parent: 3, start_date: wd(14), duration: 5, progress: 0, owner: "QA Guild" },
			{ id: 93, text: "Cost optimization pass",        parent: 3, start_date: wd(19), duration: 3, progress: 0, owner: "Dana Ruiz" },
			{ id: 94, text: "Infra runbook updates",         parent: 3, start_date: wd(23), duration: 3, progress: 0, owner: "Noah Kim" },
			{ id: 96, text: "Platform review",              parent: 3, start_date: wd(27), duration: 2, progress: 0, owner: "Security Guild" },
			{ id: 98, text: "Platform ready", parent: 3, type: "milestone", start_date: wd(30), duration: 0, progress: 0 }
		],
		links: [
			{ id: 1, source: 10, target: 40, type: "0" },
			{ id: 2, source: 40, target: 45, type: "0" },
			{ id: 3, source: 33, target: 70, type: "0" },
			{ id: 4, source: 70, target: 95, type: "0" },
			{ id: 5, source: 95, target: 99, type: "0" },
			{ id: 6, source: 80, target: 90, type: "0" },
			{ id: 7, source: 90, target: 92, type: "0" },
			{ id: 8, source: 92, target: 93, type: "0" },
			{ id: 9, source: 93, target: 94, type: "0" },
			{ id: 10, source: 94, target: 96, type: "0" },
			{ id: 11, source: 96, target: 98, type: "0" }
		]
	};
}

/* ---------------------------------------------------------------------------
 *  SCISSORS: a click cuts, while a drag still moves or resizes the segment.
 * ------------------------------------------------------------------------ */
let scissorsMode = true;   // the demo opens ready to cut
let suppressCutUntil = 0;
let hoverIdPosition = {id: null, pos: null};

function toggleScissors() {
	scissorsMode = !scissorsMode;
	applyScissorsMode();
}

/* The mode changes only click behaviour and the visual hint. Dragging and
   resizing remain enabled regardless of the toggle state. */
function applyScissorsMode() {
	DHX.toggle.setWrap("scissors_toggle", scissorsMode);
	document.body.classList.toggle("scissors", scissorsMode);
	showScissorsMessage();
}

function showScissorsMessage() {
	gantt.message({
		id: "scissors_info",
		expire: -1,
		text: scissorsMode
			? "<b>Scissors are on.</b> Click a task bar to split it. Drag or resize the same bar to reschedule it; touching or overlapping sessions merge automatically."
			: "<b>Scissors are off.</b> Drag or resize any session. Turn Scissors on when a regular click should cut the bar."
	});
}

/* Update hover task ID */
gantt.attachEvent("onMouseMove", function (id, e){
	if (!e.target.closest(".gantt_task_content")){
		return
	}

	const previousId = hoverIdPosition.id;
	hoverIdPosition.id = id || null
	if (previousId !== hoverIdPosition.id && gantt.isTaskExists(previousId)){
		gantt.refreshTask(previousId)
	}
	if (hoverIdPosition.id) {
		hoverIdPosition.pos = gantt.utils.dom.getRelativeEventPosition(e, gantt.$task_data).x;
		gantt.refreshTask(hoverIdPosition.id)
	}
});

/* A drag normally finishes with a browser click. Keep a short guard after the
   drag ends so that releasing a moved bar never cuts it accidentally. */
gantt.attachEvent("onAfterTaskDrag", function (id, mode) {
	suppressCutUntil = Date.now() + 250;
	if (mode === "move" || mode === "resize") {
		mergeConnectedSegments(id);
	}
});

gantt.attachEvent("onTaskClick", function (id, e) {
	if (!scissorsMode) return true;
	if (Date.now() < suppressCutUntil) return false;

	/* onTaskClick also fires in the grid. Only a click on the timeline bar cuts. */
	let bar = e.target.closest && e.target.closest(".gantt_task_line");
	if (!bar) return true;

	let task = gantt.getTask(id);
	let x = gantt.utils.dom.getRelativeEventPosition(e, gantt.$task_data).x;
	let cut = gantt.date.day_start(gantt.dateFromPos(x));
	if (splitTaskAt(task, cut, currentReason)) gantt.render();
	return false;
});

/* Cut `task` at `cutDate`, inserting a PAUSE_DAYS pause.
 *  - a plain leaf  -> wrapped into a new split project (engine pattern)
 *  - a split segment -> that segment is cut, the tail becomes a new segment */
function splitTaskAt(task, cutDate, reason) {
	if (gantt.hasChild(task.id)) return false;                 // swimlane / wrapper rows
	if (task.type === gantt.config.types.milestone) return false;

	let parentId = gantt.getParent(task.id);
	let parent = parentId ? gantt.getTask(parentId) : null;
	let start = task.start_date, end = task.end_date;
	if (cutDate <= start || cutDate >= end) return false;           // click must land inside the bar

	if (parent && isSplit(parent)) {
		/* already a split: shorten this segment, add the tail as a new session */
		task.end_date = new Date(cutDate);
		gantt.updateTask(task.id);
		addSession(parent.id, gantt.date.add(cutDate, PAUSE_DAYS, "day"), gantt.date.add(end, PAUSE_DAYS, "day"), task.owner);
		parent.cause = reason;
		gantt.updateTask(parent.id);
		return true;
	}

	/* plain leaf -> wrap into a split project; the original work is session one */
	let proj = gantt.addTask({ text: task.text, type: "project", render: "split", open: false, owner: task.owner, cause: reason }, parentId);
	gantt.moveTask(proj, gantt.getTaskIndex(task.id), parentId);
	gantt.moveTask(task.id, 0, proj);
	gantt.calculateTaskLevel(gantt.getTask(task.id));

	let seg1 = gantt.getTask(task.id);
	seg1.end_date = new Date(cutDate);
	gantt.updateTask(seg1.id);

	addSession(proj, gantt.date.add(cutDate, PAUSE_DAYS, "day"), gantt.date.add(end, PAUSE_DAYS, "day"), task.owner);
	return true;
}

function addSession(parentId, start, end, owner) {
	let seg = gantt.addTask({ text: "", start_date: new Date(start), end_date: new Date(end), owner: owner, progress: 0 }, parentId);
	gantt.calculateTaskLevel(gantt.getTask(seg));
	return seg;
}

/* Merge only sessions that belong to the same split parent. Independent tasks
   keep their identity even if their dates happen to overlap. */
function mergeConnectedSegments(taskId) {
	if (!gantt.isTaskExists(taskId)) return;
	let task = gantt.getTask(taskId);
	let parent = getSplitParent(task);
	if (!parent) return;

	gantt.batchUpdate(function () {
		let segments = getOrderedSegments(parent.id);
		if (segments.length < 2) return;

		let current = segments[0];
		for (let i = 1; i < segments.length; i++) {
			let next = segments[i];
			if (next.start_date <= current.end_date) {
				mergeSegmentData(current, next);
				redirectSegmentLinks(next.id, current.id);
				gantt.updateTask(current.id);
				gantt.deleteTask(next.id);
			} else {
				current = next;
			}
		}

		if (gantt.isTaskExists(parent.id) && gantt.getChildren(parent.id).length === 1) {
			unwrapSingleSegment(parent.id);
		}
	});
}

function redirectSegmentLinks(sourceId, targetId) {
	gantt.getLinks().slice().forEach(function (link) {
		let changed = false;
		if (link.source == sourceId) {
			link.source = targetId;
			changed = true;
		}
		if (link.target == sourceId) {
			link.target = targetId;
			changed = true;
		}
		if (!changed) return;

		if (link.source == link.target) gantt.deleteLink(link.id);
		else gantt.updateLink(link.id);
	});
}

function mergeSegmentData(target, source) {
	let targetSpan = Math.max(0, target.end_date - target.start_date);
	let sourceSpan = Math.max(0, source.end_date - source.start_date);
	let completed = targetSpan * (target.progress || 0) + sourceSpan * (source.progress || 0);

	if (source.start_date < target.start_date) target.start_date = new Date(source.start_date);
	if (source.end_date > target.end_date) target.end_date = new Date(source.end_date);

	let mergedSpan = Math.max(1, target.end_date - target.start_date);
	target.progress = Math.min(1, completed / mergedSpan);
	if (!target.owner && source.owner) target.owner = source.owner;
}

/* When every interruption has disappeared, remove the technical split wrapper
   and turn the remaining segment back into a regular task. */
function unwrapSingleSegment(parentId) {
	let parent = gantt.getTask(parentId);
	let childId = gantt.getChildren(parentId)[0];
	let child = gantt.getTask(childId);
	let grandParentId = gantt.getParent(parentId);
	let parentIndex = gantt.getTaskIndex(parentId);

	child.text = parent.text;
	child.owner = child.owner || parent.owner;
	gantt.moveTask(child.id, parentIndex, grandParentId);
	gantt.calculateTaskLevel(child);
	gantt.updateTask(child.id);
	gantt.deleteTask(parentId);
}

gantt.addTaskLayer(function (task) {
	if (!scissorsMode || !hoverIdPosition.id || gantt.getState().drag_id) {
		return
	}

	if (task.type === "project"){
		if (task.render == "split"){
			gantt.eachTask(function(child){
				if (hoverIdPosition.id == child.id){
					task = child;
				}
			}, task.id);
		}
		else {
			return;
		}
	}

	if (hoverIdPosition.id != task.id){
		return
	}

	let preview = document.createElement("div");
	preview.className = "split_cut_preview";
	preview.setAttribute("data-task-id", task.id);
	preview.innerHTML =
		'<span class="split_cut_icon" aria-hidden="true">' +
			'<svg viewBox="0 0 24 24"><circle cx="6" cy="7" r="3"/><circle cx="6" cy="17" r="3"/><path d="m8.7 8.4 10.3 6.1"/><path d="m8.7 15.6 10.3-6.1"/></svg>' +
		'</span>' +
		'<span class="split_cut_line"></span>';


	let cutDate = gantt.date.day_start(gantt.dateFromPos(hoverIdPosition.pos));
	const minCutDate = gantt.date.add(task.start_date, 1, "day");
	if (cutDate < minCutDate){
		cutDate = minCutDate;
	}

	let pos = gantt.getTaskPosition(task, task.start_date, task.end_date);
	preview.style.left = gantt.posFromDate(cutDate) + "px";
	preview.style.top = (pos.top - 10) + "px";
	preview.style.height = (pos.height + 18) + "px";
	preview.style.display = "block";
	return preview;
});

/* ---------------------------------------------------------------------------
 *  Pause markers: a connector + labelled pill in each gap between sessions.
 *  Computed from the actual segment children, so it follows live edits too.
 * ------------------------------------------------------------------------ */
gantt.addTaskLayer(function (task) {
	if (!isSplit(task) || task.$open) return false;
	let childIds = gantt.getChildren(task.id);
	if (!childIds || childIds.length < 2) return false;

	let kids = childIds.map(function (id) { return gantt.getTask(id); })
		.sort(function (x, y) { return x.start_date - y.start_date; });

	let wrap = document.createElement("div");
	wrap.className = "gap_layer";
	let drew = false;

	for (let i = 0; i < kids.length - 1; i++) {
		let gapStart = kids[i].end_date, gapEnd = kids[i + 1].start_date;
		if (gapEnd - gapStart < 12 * 60 * 60 * 1000) continue;

		let pos  = gantt.getTaskPosition(task, gapStart, gapEnd);
		let yMid = pos.top + (pos.rowHeight || gantt.config.row_height) / 2;

		let conn = document.createElement("div");
		conn.className = "gap_conn";
		conn.style.left  = pos.left + "px";
		conn.style.width = pos.width + "px";
		conn.style.top   = yMid + "px";
		wrap.appendChild(conn);

		let pill = document.createElement("div");
		pill.className = "gap_pill cause_" + (task.cause || "break");
		pill.textContent = CAUSE_LABELS[task.cause] || "Paused";
		pill.style.left = (pos.left + pos.width / 2) + "px";
		pill.style.top  = yMid + "px";
		wrap.appendChild(pill);

		drew = true;
	}
	return drew ? wrap : false;
});

/* ---------------------------------------------------------------------------
 *  Reset back to the authored example.
 * ------------------------------------------------------------------------ */
function resetExample() {
	gantt.clearAll();
	gantt.parse(buildData());
}

DHX.dd.initClickOutside();
buildReasonMenu();
document.body.classList.add("scissors");   // scissors start on (see toggle default)

gantt.init("gantt_here");
gantt.parse(buildData());
applyScissorsMode();
<!DOCTYPE html>
<html lang="en">
<head>
	<meta http-equiv="Content-type" content="text/html; charset=utf-8">
	<title>Split tasks: cut a task where the work stops | dhtmlxGantt</title>
	<script src="../../codebase/dhtmlxgantt.js?v=10.0.0"></script>
	<link rel="stylesheet" href="../../codebase/dhtmlxgantt.css?v=10.0.0">
	<link rel="stylesheet" href="../common/demo-controls/dhx_controls.css">
	<script src="../common/demo-controls/dhx_controls.js"></script>
	<link rel="preconnect" href="https://fonts.googleapis.com">
	<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
	<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap">
	<style>
		body { display: flex; flex-direction: column; }
		#gantt_here { flex: 1 1 auto; min-height: 0; width: 100%; }

		.gantt_task_line { border: none; border-radius: 7px; box-shadow: 0 1px 2px rgba(16,24,40,.18); }
		.gantt_task_line .gantt_task_progress { border-radius: 7px 0 0 7px; }
		.gantt_task_line.gantt_project { border-radius: 5px; }

		.gantt_task_cell.weekend, .gantt_scale_cell.weekend { background-color: rgba(15,23,42,.04); }

		.split_cut_preview {
			position: absolute;
			display: none;
			width: 1px;
			pointer-events: none;
			z-index: 6;
		}
		.split_cut_line {
			position: absolute;
			left: 0;
			top: 18px;
			width: 2px;
			height: calc(100% - 18px);
			transform: translateX(-1px);
			background: repeating-linear-gradient(
				to bottom,
				#c62a30 0,
				#c62a30 5px,
				transparent 5px,
				transparent 9px
			);
			background-position: 0 0;
			animation: split_cut_dash 420ms linear infinite;
			filter: drop-shadow(0 0 1px rgba(255,255,255,.85));
		}
		.split_cut_icon {
			position: absolute;
			left: 0;
			top: 0;
			width: 18px;
			height: 18px;
			transform: translateX(-50%);
			display: flex;
			align-items: center;
			justify-content: center;
			border-radius: 50%;
			background: var(--dhx-panel);
			color: #c62a30;
			box-shadow: 0 1px 4px rgba(16,24,40,.24);
		}
		.split_cut_icon svg {
			width: 12px;
			height: 12px;
			fill: none;
			stroke: currentColor;
			stroke-width: 2;
			stroke-linecap: round;
			stroke-linejoin: round;
		}
		@keyframes split_cut_dash {
			to { background-position: 0 9px; }
		}

		.gap_layer { position: absolute; left: 0; top: 0; pointer-events: none; z-index: 2; }
		.gap_conn  { position: absolute; height: 0; border-top: 2px dotted #b7c3d4; transform: translateY(-50%); }
		.gap_pill  {
			position: absolute; transform: translate(-50%, -50%);
			display: inline-flex; align-items: center; gap: 6px;
			padding: 2px 9px; border-radius: 11px;
			background: var(--dhx-surface); border: 1px solid var(--dhx-line);
			box-shadow: 0 1px 2px rgba(16,24,40,.16);
			font: 600 11px var(--dhx-font); color: var(--dhx-ink-2); white-space: nowrap;
		}
		.gap_pill::before { content: ""; width: 7px; height: 7px; border-radius: 50%; flex: none; }

		.cause_vacation .cause_dot, .cause_vacation.gap_pill::before { background: #1d6fd6; }
		.cause_oncall   .cause_dot, .cause_oncall.gap_pill::before   { background: #7c3aed; }
		.cause_hotfix   .cause_dot, .cause_hotfix.gap_pill::before   { background: #c0392b; }
		.cause_training .cause_dot, .cause_training.gap_pill::before { background: #b8740a; }
		.cause_leave    .cause_dot, .cause_leave.gap_pill::before    { background: #1f8a4c; }
		.cause_break    .cause_dot, .cause_break.gap_pill::before    { background: #64748b; }

		.cause_chip { display: inline-flex; align-items: center; gap: 6px; font: 600 12px var(--dhx-font); color: var(--dhx-ink-2); }
		.cause_chip .cause_dot { width: 8px; height: 8px; border-radius: 50%; }
		.muted_dash { color: var(--dhx-muted); }

		.split_row .gantt_tree_icon.gantt_open,
		.split_row .gantt_tree_icon.gantt_close { visibility: hidden; pointer-events: none; }

		.toolbar_label { font: 600 13px var(--dhx-font); color: var(--dhx-ink-2); }
		.reason_label { color: var(--dhx-ink); }
	</style>
</head>
<body>

<div class="dhx_header" style="justify-content:center; gap:14px">
	<span class="toolbar_label">Sprint 24 plan</span>

	<label class="dhx_toggle active" id="scissors_toggle" onclick="toggleScissors()">
		<span>&#9986; Scissors</span><span class="dhx_toggle__track on"></span>
	</label>

	<div class="dhx_dd" id="reason_dd">
		<button class="dhx_btn dhx_btn--surface" onclick="DHX.dd.toggle('reason_dd')">
			<span>Pause reason: <b class="reason_label" id="reason_label">On-call</b></span>
			<span class="dhx_caret"><svg class="dhx_ic dhx_ic--sm" viewBox="0 0 24 24"><polyline points="6 9 12 15 18 9"/></svg></span>
		</button>
		<div class="dhx_menu" id="reason_menu"></div>
	</div>

	<button class="dhx_btn dhx_btn--surface" title="Restore the starting plan" onclick="resetExample()">Reset example</button>
</div>

<div id="gantt_here"></div>


</body>
</html>