← Hen ScreenshotsCONTENT HISTORY

Update to Hen Screenshots

Snapshot Oct 24, 2026 · 13:21 UTC · version 0.2.8

WHAT CHANGED

Package or technical metadata updated

Package or technical metadata updated

Observed in package metadata. These changes alone do not establish a new customer-facing feature.

Save Hen Screenshots and check future changes in Following.

Up to 3 plugins free · no card required.

All fields and original values.

These are saved file metadata. Open a changed file to read its text edits.

Package file

Before

Sep 30, 2026 · 23:16 UTC

bb2f8bd86f949e5260ce40f1c3215672ba0f1e576284dd152053cca7ac90afaf

After

Oct 24, 2026 · 13:21 UTC

b9cbe58f74b25ba251d3a0205c6b25dd41276d21348fa817ebd5dc9dfe92514e

Package file

Before

Sep 30, 2026 · 23:16 UTC

519

After

Oct 24, 2026 · 13:21 UTC

543

Package file

Before

Sep 30, 2026 · 23:16 UTC

{"sha256":"5abf911a83f89a2a655d94ec0bad78a3b9f633388159a9de6305485ee7d910ff","size":14032}

After

Oct 24, 2026 · 13:21 UTC

Not present

Package file

Before

Sep 30, 2026 · 23:16 UTC

{"sha256":"5abf911a83f89a2a655d94ec0bad78a3b9f633388159a9de6305485ee7d910ff","size":14032}

After

Oct 24, 2026 · 13:21 UTC

Not present

Compare saved observations

Download comparison JSON

dist/cli.mjs

− removed · + added

--- before
+++ after
@@ -8,7 +8,7 @@
 import "konva/skia-backend";
 import { FontLibrary, Image as Image$1 } from "skia-canvas";
 //#region plugins/hen-screenshots/package.json
-var version = "0.2.4";
+var version = "0.2.8";
 //#endregion
 //#region src/core/device-composition-spec.ts
 var deviceCompositions = {
@@ -57,6 +57,303 @@
 	return panoramaStart(id) !== void 0;
 }
 //#endregion
+//#region src/core/additional-store-formats.ts
+var APPLE_SCREENSHOTS$1 = "https://developer.apple.com/help/app-store-connect/reference/app-information/screenshot-specifications/";
+var PLAY_SCREENSHOTS$1 = "https://support.google.com/googleplay/android-developer/answer/9866151?hl=en";
+var additionalStoreProfiles = [
+	{
+		id: "apple-iphone63-portrait",
+		name: "App Store · iPhone · 6.3-inch · Portrait",
+		store: "apple",
+		category: "phone",
+		width: 1206,
+		height: 2622,
+		maxCount: 10,
+		note: "Optional device slot; larger screenshots can be scaled by App Store Connect.",
+		source: APPLE_SCREENSHOTS$1
+	},
+	{
+		id: "apple-iphone63-landscape",
+		name: "App Store · iPhone · 6.3-inch · Landscape",
+		store: "apple",
+		category: "phone",
+		width: 2622,
+		height: 1206,
+		maxCount: 10,
+		note: "Optional device slot; larger screenshots can be scaled by App Store Connect.",
+		source: APPLE_SCREENSHOTS$1
+	},
+	{
+		id: "apple-iphone61-portrait",
+		name: "App Store · iPhone · 6.1-inch · Portrait",
+		store: "apple",
+		category: "phone",
+		width: 1170,
+		height: 2532,
+		maxCount: 10,
+		note: "Optional device slot; larger screenshots can be scaled by App Store Connect.",
+		source: APPLE_SCREENSHOTS$1
+	},
+	{
+		id: "apple-iphone61-landscape",
+		name: "App Store · iPhone · 6.1-inch · Landscape",
+		store: "apple",
+		category: "phone",
+		width: 2532,
+		height: 1170,
+		maxCount: 10,
+		note: "Optional device slot; larger screenshots can be scaled by App Store Connect.",
+		source: APPLE_SCREENSHOTS$1
+	},
+	{
+		id: "apple-iphone55-portrait",
+		name: "App Store · iPhone · 5.5-inch · Portrait",
+		store: "apple",
+		category: "phone",
+		width: 1242,
+		height: 2208,
+		maxCount: 10,
+		note: "Optional device slot; larger screenshots can be scaled by App Store Connect.",
+		source: APPLE_SCREENSHOTS$1
+	},
+	{
+		id: "apple-iphone55-landscape",
+		name: "App Store · iPhone · 5.5-inch · Landscape",
+		store: "apple",
+		category: "phone",
+		width: 2208,
+		height: 1242,
+		maxCount: 10,
+		note: "Optional device slot; larger screenshots can be scaled by App Store Connect.",
+		source: APPLE_SCREENSHOTS$1
+	},
+	{
+		id: "apple-iphone47-portrait",
+		name: "App Store · iPhone · 4.7-inch · Portrait",
+		store: "apple",
+		category: "phone",
+		width: 750,
+		height: 1334,
+		maxCount: 10,
+		note: "Optional device slot; larger screenshots can be scaled by App Store Connect.",
+		source: APPLE_SCREENSHOTS$1
+	},
+	{
+		id: "apple-iphone47-landscape",
+		name: "App Store · iPhone · 4.7-inch · Landscape",
+		store: "apple",
+		category: "phone",
+		width: 1334,
+		height: 750,
+		maxCount: 10,
+		note: "Optional device slot; larger screenshots can be scaled by App Store Connect.",
+		source: APPLE_SCREENSHOTS$1
+	},
+	{
+		id: "apple-iphone4-portrait",
+		name: "App Store · iPhone · 4-inch · Portrait",
+		store: "apple",
+		category: "phone",
+		width: 640,
+		height: 1136,
+		maxCount: 10,
+		note: "Optional device slot; larger screenshots can be scaled by App Store Connect.",
+		source: APPLE_SCREENSHOTS$1
+	},
+	{
+		id: "apple-iphone4-landscape",
+		name: "App Store · iPhone · 4-inch · Landscape",
+		store: "apple",
+		category: "phone",
+		width: 1136,
+		height: 640,
+		maxCount: 10,
+		note: "Optional device slot; larger screenshots can be scaled by App Store Connect.",
+		source: APPLE_SCREENSHOTS$1
+	},
+	{
+		id: "apple-iphone35-portrait",
+		name: "App Store · iPhone · 3.5-inch · Portrait",
+		store: "apple",
+		category: "phone",
+		width: 640,
+		height: 960,
+		maxCount: 10,
+		note: "Optional device slot; larger screenshots can be scaled by App Store Connect.",
+		source: APPLE_SCREENSHOTS$1
+	},
+	{
+		id: "apple-iphone35-landscape",
+		name: "App Store · iPhone · 3.5-inch · Landscape",
+		store: "apple",
+		category: "phone",
+		width: 960,
+		height: 640,
+		maxCount: 10,
+		note: "Optional device slot; larger screenshots can be scaled by App Store Connect.",
+		source: APPLE_SCREENSHOTS$1
+	},
+	{
+		id: "apple-ipad129-portrait",
+		name: "App Store · iPad · 12.9-inch · Portrait",
+		store: "apple",
+		category: "tablet",
+		width: 2048,
+		height: 2732,
+		maxCount: 10,
+		note: "Optional device slot; larger screenshots can be scaled by App Store Connect.",
+		source: APPLE_SCREENSHOTS$1
+	},
+	{
+		id: "apple-ipad129-landscape",
+		name: "App Store · iPad · 12.9-inch · Landscape",
+		store: "apple",
+		category: "tablet",
+		width: 2732,
+		height: 2048,
+		maxCount: 10,
+		note: "Optional device slot; larger screenshots can be scaled by App Store Connect.",
+		source: APPLE_SCREENSHOTS$1
+	},
+	{
+		id: "apple-ipad105-portrait",
+		name: "App Store · iPad · 10.5-inch · Portrait",
+		store: "apple",
+		category: "tablet",
+		width: 1668,
+		height: 2224,
+		maxCount: 10,
+		note: "Optional device slot; larger screenshots can be scaled by App Store Connect.",
+		source: APPLE_SCREENSHOTS$1
+	},
+	{
+		id: "apple-ipad105-landscape",
+		name: "App Store · iPad · 10.5-inch · Landscape",
+		store: "apple",
+		category: "tablet",
+		width: 2224,
+		height: 1668,
+		maxCount: 10,
+		note: "Optional device slot; larger screenshots can be scaled by App Store Connect.",
+		source: APPLE_SCREENSHOTS$1
+	},
+	{
+		id: "apple-ipad97-portrait",
+		name: "App Store · iPad · 9.7-inch · Portrait",
+		store: "apple",
+		category: "tablet",
+		width: 1536,
+		height: 2048,
+		maxCount: 10,
+		note: "Optional device slot; larger screenshots can be scaled by App Store Connect.",
+		source: APPLE_SCREENSHOTS$1
+	},
+	{
+		id: "apple-ipad97-landscape",
+		name: "App Store · iPad · 9.7-inch · Landscape",
+		store: "apple",
+		category: "tablet",
+		width: 2048,
+		height: 1536,
+		maxCount: 10,
+		note: "Optional device slot; larger screenshots can be scaled by App Store Connect.",
+		source: APPLE_SCREENSHOTS$1
+	},
+	{
+		id: "play-tablet7-8k-portrait",
+		name: "Google Play · Tablet · 7-inch · 8K · Portrait",
+		store: "google",
+		category: "tablet",
+		width: 4320,
+		height: 7680,
+		maxCount: 8,
+		note: "Large-screen screenshot: 16:9 or 9:16, up to 7680 pixels.",
+		source: PLAY_SCREENSHOTS$1
+	},
+	{
+		id: "play-tablet7-8k-landscape",
+		name: "Google Play · Tablet · 7-inch · 8K · Landscape",
+		store: "google",
+		category: "tablet",
+		width: 7680,
+		height: 4320,
+		maxCount: 8,
+		note: "Large-screen screenshot: 16:9 or 9:16, up to 7680 pixels.",
+		source: PLAY_SCREENSHOTS$1
+	},
+	{
+		id: "play-tablet10-8k-portrait",
+		name: "Google Play · Tablet · 10-inch · 8K · Portrait",
+		store: "google",
+		category: "tablet",
+		width: 4320,
+		height: 7680,
+		maxCount: 8,
+		note: "Large-screen screenshot: 16:9 or 9:16, up to 7680 pixels.",
+		source: PLAY_SCREENSHOTS$1
+	},
+	{
+		id: "play-tablet10-8k-landscape",
+		name: "Google Play · Tablet · 10-inch · 8K · Landscape",
+		store: "google",
+		category: "tablet",
+		width: 7680,
+		height: 4320,
+		maxCount: 8,
+		note: "Large-screen screenshot: 16:9 or 9:16, up to 7680 pixels.",
+		source: PLAY_SCREENSHOTS$1
+	},
+	{
+		id: "play-chromebook-8k-landscape",
+		name: "Google Play · Chromebook · 8K · Landscape",
+		store: "google",
+		category: "desktop",
+		width: 7680,
+		height: 4320,
+		maxCount: 8,
+		note: "Large-screen screenshot: 16:9 or 9:16, up to 7680 pixels.",
+		source: PLAY_SCREENSHOTS$1
+	}
+];
+var appleFrameVariants = {
+	"apple-iphone63": {
+		ratio: 2622 / 1206,
+		hardware: "island"
+	},
+	"apple-iphone61": {
+		ratio: 2532 / 1170,
+		hardware: "notch"
+	},
+	"apple-iphone55": {
+		ratio: 2208 / 1242,
+		hardware: "home"
+	},
+	"apple-iphone47": {
+		ratio: 1334 / 750,
+		hardware: "home"
+	},
+	"apple-iphone4": {
+		ratio: 1136 / 640,
+		hardware: "home"
+	},
+	"apple-iphone35": {
+		ratio: 960 / 640,
+		hardware: "home"
+	},
+	"apple-ipad129": {
+		ratio: 2732 / 2048,
+		hardware: "home"
+	},
+	"apple-ipad105": {
+		ratio: 2224 / 1668,
+		hardware: "home"
+	},
+	"apple-ipad97": {
+		ratio: 2048 / 1536,
+		hardware: "home"
+	}
+};
+//#endregion
 //#region src/core/special-profiles.ts
 var specialProfiles = [
 	{
@@ -238,6 +535,7 @@
 }
 var exportProfiles = [
 	...specialProfiles,
+	...additionalStoreProfiles,
 	{
 		id: "play-feature-graphic",
 		name: "Google Play · Feature graphic",
@@ -404,6 +702,28 @@
 		source: APPLE_SCREENSHOTS
 	},
 	{
+		id: "apple-ipad11-portrait",
+		name: "App Store · iPad 11-inch · Portrait",
+		store: "apple",
+		category: "tablet",
+		width: 1668,
+		height: 2420,
+		maxCount: 10,
+		note: "An accepted size for the 11-inch iPad slot.",
+		source: APPLE_SCREENSHOTS
+	},
+	{
+		id: "apple-ipad11-landscape",
+		name: "App Store · iPad 11-inch · Landscape",
+		store: "apple",
+		category: "tablet",
+		width: 2420,
+		height: 1668,
+		maxCount: 10,
+		note: "An accepted size for the 11-inch iPad slot.",
+		source: APPLE_SCREENSHOTS
+	},
+	{
 		id: "apple-mac",
 		name: "App Store · Mac",
 		store: "apple",
@@ -521,7 +841,7 @@
 /** Size rules are separate from the allowlisted preset values, so bad edits fail closed. */
 function validateDimensions(profile, width, height) {
 	if (width !== profile.width || height !== profile.height || !Number.isInteger(width) || !Number.isInteger(height)) throw new Error("The exported image does not match the selected preset dimensions.");
-	const special = specialProfiles.find((item) => item.id === profile.id);
+	const special = [...specialProfiles, ...additionalStoreProfiles].find((item) => item.id === profile.id);
 	if (special) {
 		if (profile.store !== special.store || profile.category !== special.category || width !== special.width || height !== special.height) throw new Error("These dimensions do not match the selected store device slot.");
 		return;
@@ -538,7 +858,7 @@
 	});
 	if (profile.store === "google") {
 		const short = Math.min(width, height), long = Math.max(width, height);
-		if (short < 320 || long > 3840 || long > short * 2) throw new Error("These dimensions do not satisfy Google Play screenshot rules.");
+		if (short < 320 || long > (profile.category === "phone" ? 3840 : 7680) || long > short * 2) throw new Error("These dimensions do not satisfy Google Play screenshot rules.");
 		if (profile.category !== "phone" && (short < 1080 || long * 9 !== short * 16)) throw new Error("Google Play large-screen presets require 16:9 or 9:16 and at least 1080 pixels.");
 	} else if (profile.store === "apple") {
 		if (!{
@@ -555,7 +875,16 @@
 				[1260, 2736],
 				[2736, 1260]
 			],
-			tablet: [
+			tablet: profile.id.includes("ipad11") ? [
+				[1488, 2266],
+				[2266, 1488],
+				[1668, 2420],
+				[2420, 1668],
+				[1668, 2388],
+				[2388, 1668],
+				[1640, 2360],
+				[2360, 1640]
+			] : [
 				[2064, 2752],
 				[2752, 2064],
 				[2048, 2732],
@@ -763,7 +1092,7 @@
 	}
 };
 var LIMITS = {
-	shots: 20,
+	shots: 50,
 	overlays: 8,
 	assetBytes: 52428800,
 	totalBytes: 125829120,
@@ -806,6 +1135,10 @@
 	"orbit",
 	"orbit-end",
 	"workbench",
+	"portfolio-laptop",
+	"portfolio-monitor",
+	"portfolio-laptop-editorial",
+	"portfolio-monitor-dark",
 	"nocturne",
 	"carbon",
 	"ember",
@@ -1344,22 +1677,27 @@
 function positive(value, name) {
 	if (!Number.isFinite(value) || value <= 0) throw new Error(`${name} must be a positive number.`);
 }
-function handheldGeometry(family, width, frame) {
+function handheldGeometry(family, width, frame, exportProfile) {
 	const tablet = family === "ipad" || family === "android-tablet";
-	const inset = frame ? width * (tablet ? .035 : family === "ios" ? .027 : .024) : 0;
+	const variant = family === "ipad" && exportProfile?.startsWith("apple-ipad") || family === "ios" && exportProfile?.startsWith("apple-iphone") ? appleFrameVariants[exportProfile?.replace(/-(portrait|landscape)$/, "") ?? ""] : void 0;
+	const home = variant?.hardware === "home";
+	const notch = variant?.hardware === "notch";
+	const ipad11 = exportProfile?.startsWith("apple-ipad11-") ?? false;
+	const inset = frame ? width * (home ? .045 : tablet ? .035 : family === "ios" ? .027 : .024) : 0;
+	const verticalInset = frame && home ? width * .14 : inset;
 	const screenWidth = width - inset * 2;
-	const screenHeight = screenWidth * (family === "ipad" ? 4 / 3 : family === "android-tablet" ? 16 / 10 : family === "ios" ? 19.5 / 9 : 20 / 9);
-	const radius = tablet ? frame ? width * (family === "ipad" ? .058 : .043) : 0 : width * (family === "ios" ? frame ? .13 : .102 : .078);
+	const screenHeight = screenWidth * (variant?.ratio ?? (family === "ipad" ? ipad11 ? 2420 / 1668 : 4 / 3 : family === "android-tablet" ? 16 / 10 : family === "ios" ? 19.5 / 9 : 20 / 9));
+	const radius = home ? frame ? width * .055 : 0 : tablet ? frame ? width * (family === "ipad" ? .058 : .043) : 0 : width * (family === "ios" ? frame ? .13 : .102 : .078);
 	const screen = {
 		x: inset,
-		y: inset,
+		y: verticalInset,
 		width: screenWidth,
 		height: screenHeight,
-		radius: Math.max(0, radius - inset)
+		radius: home ? 0 : Math.max(0, radius - inset)
 	};
-	const cameraWidth = tablet ? width * .009 : family === "ios" ? screenWidth * .29 : screenWidth * .032;
-	const cameraHeight = family === "ios" ? screenWidth * .081 : cameraWidth;
-	const height = screenHeight + inset * 2;
+	const cameraWidth = home ? width * .013 : notch ? screenWidth * .42 : tablet ? width * .009 : family === "ios" ? screenWidth * .29 : screenWidth * .032;
+	const cameraHeight = home ? cameraWidth : notch ? screenWidth * .075 : family === "ios" ? screenWidth * .081 : cameraWidth;
+	const height = screenHeight + verticalInset * 2;
 	const edge = width * (family === "ios" ? .006 : .004);
 	const sideButton = (right, y, length) => ({
 		x: right ? width - edge * 1.5 : 0,
@@ -1393,9 +1731,17 @@
 		height,
 		radius,
 		screen,
+		cameraKind: home || tablet || family !== "ios" ? "lens" : notch ? "notch" : "island",
+		...home && frame ? { homeButton: {
+			x: width * .465,
+			y: height - verticalInset / 2 - width * .035,
+			width: width * .07,
+			height: width * .07,
+			radius: width * .035
+		} } : {},
 		camera: {
-			x: tablet && frame ? width - (inset + cameraWidth) / 2 : (width - cameraWidth) / 2,
-			y: tablet ? frame ? (height - cameraHeight) / 2 : width * .012 : inset + screenWidth * (family === "ios" ? .026 : .022),
+			x: !home && tablet && frame ? width - (inset + cameraWidth) / 2 : (width - cameraWidth) / 2,
+			y: home ? verticalInset * .42 : notch ? inset : tablet ? frame ? (height - cameraHeight) / 2 : width * .012 : inset + screenWidth * (family === "ios" ? .026 : .022),
 			width: cameraWidth,
 			height: cameraHeight,
 			radius: cameraHeight / 2
@@ -1427,7 +1773,7 @@
 	};
 }
 /** Generic geometry. Width covers the entire device, including a laptop's base. */
-function deviceGeometry(family, width, frame, orientation = "portrait") {
+function deviceGeometry(family, width, frame, orientation = "portrait", exportProfile) {
 	positive(width, "Device width");
 	if (family === "card") {
 		const height = width * (orientation === "landscape" ? 3 / 4 : 4 / 3);
@@ -1591,8 +1937,8 @@
 		"ipad",
 		"android-tablet"
 	].includes(family)) throw new Error("This device frame is not supported.");
-	if (orientation === "portrait") return handheldGeometry(family, width, frame);
-	const portrait = handheldGeometry(family, width / handheldGeometry(family, 1, frame).height, frame);
+	if (orientation === "portrait") return handheldGeometry(family, width, frame, exportProfile);
+	const portrait = handheldGeometry(family, width / handheldGeometry(family, 1, frame, exportProfile).height, frame, exportProfile);
 	const rotate = (rect) => ({
 		x: rect.y,
 		y: portrait.width - rect.x - rect.width,
@@ -1606,6 +1952,7 @@
 		height: portrait.width,
 		screen: rotate(portrait.screen),
 		camera: rotate(portrait.camera),
+		...portrait.homeButton ? { homeButton: rotate(portrait.homeButton) } : {},
 		...portrait.handheld ? { handheld: {
 			shell: rotate(portrait.handheld.shell),
 			buttons: portrait.handheld.buttons.map(rotate),
@@ -1900,9 +2247,8 @@
 function linkedShots(project, shotId) {
 	return panoramaPair(project, shotId) ?? project.shots.filter((shot) => shot.id === shotId);
 }
-function shotCapacity(project) {
-	const profile = resolveExportProfile(project);
-	return profile.category === "banner" ? LIMITS.shots : Math.min(LIMITS.shots, profile.maxCount);
+function shotCapacity(_project) {
+	return LIMITS.shots;
 }
 var panoramaStyle = {
 	template: "panorama",
@@ -1956,7 +2302,7 @@
 		titleSize: 116
 	}
 };
-function collectionLayout$1(h, style, id) {
+function collectionLayout$1(h, style, id, exportProfile) {
 	const { composition, rotation, centerX: preferredCenter } = collectionPanoramaLayouts[id];
 	const right = isPanoramaEnd(style.template);
 	const wide = h <= 1080;
@@ -1970,11 +2316,11 @@
 		y: h * .08,
 		height: h * .84
 	};
-	const unit = deviceGeometry(style.device, 1e3, style.frame, style.deviceOrientation);
+	const unit = deviceGeometry(style.device, 1e3, style.frame, style.deviceOrientation, exportProfile);
 	const angle = Math.abs(rotation) * Math.PI / 180;
 	const scale = Math.min(area.width / (Math.cos(angle) * unit.width + Math.sin(angle) * unit.height), area.height / (Math.sin(angle) * unit.width + Math.cos(angle) * unit.height));
 	const width = Math.max(32, Math.min(2160, Math.floor(unit.width * scale)));
-	const height = deviceGeometry(style.device, width, style.frame, style.deviceOrientation).height;
+	const height = deviceGeometry(style.device, width, style.frame, style.deviceOrientation, exportProfile).height;
 	const boundWidth = Math.cos(angle) * width + Math.sin(angle) * height;
 	const centerX = 1080 + Math.min(preferredCenter - 1080, boundWidth * .2);
 	const low = composition === "reverse" ? !right : right;
@@ -2015,19 +2361,19 @@
 function panoramaLayout(project, style) {
 	const { height: h } = canonicalCanvas(project);
 	const collection = collectionPanoramaId(style.template);
-	if (collection) return collectionLayout$1(h, style, collection);
+	if (collection) return collectionLayout$1(h, style, collection, project.exportProfile);
 	const right = isPanoramaEnd(style.template);
 	const family = panoramaStart(style.template);
 	const expressive = family !== "panorama";
 	const wide = h <= 1080;
 	const rotation = family === "daybreak" ? 16 : family === "tidal" ? -10 : family === "orbit" ? 12 : family === "moonlight" ? -12 : -8;
-	const unit = deviceGeometry(style.device, 1e3, style.frame, style.deviceOrientation);
+	const unit = deviceGeometry(style.device, 1e3, style.frame, style.deviceOrientation, project.exportProfile);
 	const radians = Math.abs(rotation) * Math.PI / 180;
 	const boundW = Math.cos(radians) * unit.width + Math.sin(radians) * unit.height;
 	const boundH = Math.sin(radians) * unit.width + Math.cos(radians) * unit.height;
 	const scale = Math.min((expressive ? 1100 : 980) / boundW, h * .85 / boundH);
 	const width = Math.floor(unit.width * scale);
-	const height = deviceGeometry(style.device, width, style.frame, style.deviceOrientation).height;
+	const height = deviceGeometry(style.device, width, style.frame, style.deviceOrientation, project.exportProfile).height;
 	if (expressive) return {
 		phone: {
 			x: Math.round(1080 - width / 2),
@@ -2601,6 +2947,102 @@
 	};
 }
 //#endregion
+//#region src/core/portfolio-templates.ts
+/** Explicit hardware presets; other single-device templates keep the user's frame. */
+var portfolioTemplates = [
+	{
+		id: "portfolio-laptop",
+		name: "Laptop Studio",
+		device: "laptop",
+		appearance: "light",
+		colors: [
+			"#ECEFEA",
+			"#D7DED4",
+			"#2C382E",
+			"#72856C"
+		],
+		description: "A spacious laptop cover for your next case study."
+	},
+	{
+		id: "portfolio-monitor",
+		name: "Monitor Gallery",
+		device: "monitor",
+		appearance: "light",
+		colors: [
+			"#F4EDE3",
+			"#E6D8C7",
+			"#43382C",
+			"#A1805F"
+		],
+		description: "A desktop centerpiece on a warm gallery wall."
+	},
+	{
+		id: "portfolio-laptop-editorial",
+		name: "Laptop Editorial",
+		device: "laptop",
+		appearance: "light",
+		colors: [
+			"#F2ECE0",
+			"#E2D7C5",
+			"#3C352E",
+			"#987352"
+		],
+		description: "Warm paper, a serif headline, and your work on a laptop."
+	},
+	{
+		id: "portfolio-monitor-dark",
+		name: "Monitor Focus",
+		device: "monitor",
+		appearance: "dark",
+		colors: [
+			"#232C30",
+			"#344248",
+			"#F0EDE4",
+			"#97B6B8"
+		],
+		description: "A quiet dark stage for detailed desktop interfaces."
+	}
+].map((design) => {
+	const template = defineTemplate({
+		id: design.id,
+		name: design.name,
+		category: design.id === "portfolio-laptop-editorial" ? "Editorial" : "Minimal",
+		appearance: design.appearance,
+		composition: "desk",
+		description: design.description,
+		note: "Sets up a landscape hardware frame with a large screenshot area, a headline above, and a caption below. Your text and image are preserved.",
+		surfaceLabel: design.device === "laptop" ? "Laptop showcase" : "Monitor showcase",
+		keywords: [
+			"portfolio",
+			"case study",
+			"desktop",
+			"web",
+			"website",
+			"hardware",
+			"laptop",
+			"monitor",
+			"portafolio",
+			"computadora",
+			design.device
+		],
+		...design.id === "portfolio-laptop-editorial" ? {
+			titleFont: "Fraunces",
+			titleWeight: "600"
+		} : {}
+	}, design.colors, { size: 100 });
+	return {
+		...template,
+		style: {
+			...template.style,
+			device: design.device,
+			deviceOrientation: "landscape",
+			frame: true,
+			camera: false,
+			fit: "contain"
+		}
+	};
+});
+//#endregion
 //#region src/core/device-composition.ts
 function companionFor(shot, element) {
 	return shot.companions?.find((device) => `device:${device.id}` === element);
@@ -2739,7 +3181,7 @@
 	].includes(id);
 	const boxes = frames.map((frame, index) => {
 		const pose = { ...poses[index] };
-		const size = deviceGeometry(frame.device, pose.width, frame.frame, frame.deviceOrientation);
+		const size = deviceGeometry(frame.device, pose.width, frame.frame, frame.deviceOrientation, project.exportProfile);
 		if (mirrored) {
 			pose.x = 1e3 - pose.x - size.width;
 			pose.rotation *= -1;
@@ -2764,7 +3206,7 @@
 	const devices = boxes.map((box, index) => {
 		const width = Math.max(PLACEMENT_LIMITS.width.min, box.width * scale);
 		const frame = frames[index];
-		const geometry = deviceGeometry(frame.device, width, frame.frame, frame.deviceOrientation);
+		const geometry = deviceGeometry(frame.device, width, frame.frame, frame.deviceOrientation, project.exportProfile);
 		const angle = Math.abs(box.rotation) * Math.PI / 180;
 		const rotatedWidth = Math.cos(angle) * geometry.width + Math.sin(angle) * geometry.height;
 		const rotatedHeight = Math.sin(angle) * geometry.width + Math.cos(angle) * geometry.height;
@@ -3662,6 +4104,7 @@
 //#region src/core/templates.ts
 /** IDs and geometry are part of document v2. Add new IDs for incompatible designs. */
 var templates = [
+	...portfolioTemplates,
 	...collectionPanoramaTemplates,
 	...holidayTemplates,
 	...multiDeviceTemplates,
@@ -4328,13 +4771,13 @@
 		};
 	}
 	const rotation = style.template === "tilt" ? -6 : 0;
-	const unit = deviceGeometry(style.device, 1e3, style.frame, style.deviceOrientation);
+	const unit = deviceGeometry(style.device, 1e3, style.frame, style.deviceOrientation, project.exportProfile);
 	const radians = Math.abs(rotation) * Math.PI / 180;
 	const boundW = Math.cos(radians) * unit.width + Math.sin(radians) * unit.height;
 	const boundH = Math.sin(radians) * unit.width + Math.cos(radians) * unit.height;
 	const scale = Math.min(area.width / boundW, area.height / boundH);
 	const width = Math.round(unit.width * scale);
-	const height = deviceGeometry(style.device, width, style.frame, style.deviceOrientation).height;
+	const height = deviceGeometry(style.device, width, style.frame, style.deviceOrientation, project.exportProfile).height;
 	const phone = {
 		width,
 		x: Math.round(area.x + (area.width - width) / 2),
@@ -4452,13 +4895,13 @@
 			height: area.height * (1 - inset * 2)
 		};
 	}
-	const unit = deviceGeometry(style.device, 1e3, style.frame, style.deviceOrientation);
+	const unit = deviceGeometry(style.device, 1e3, style.frame, style.deviceOrientation, project.exportProfile);
 	const rotation = template.composition ? template.phone.rotation : style.template === "punch" ? -6 : 0;
 	const radians = Math.abs(rotation) * Math.PI / 180;
 	const boundW = Math.cos(radians) * unit.width + Math.sin(radians) * unit.height;
 	const boundH = Math.sin(radians) * unit.width + Math.cos(radians) * unit.height;
 	const width = Math.floor(rotation === 0 ? Math.min(area.width, area.height / unit.height * 1e3) : 1e3 * Math.min(area.width / boundW, area.height / boundH));
-	const height = deviceGeometry(style.device, width, style.frame, style.deviceOrientation).height;
+	const height = deviceGeometry(style.device, width, style.frame, style.deviceOrientation, project.exportProfile).height;
 	return {
 		...template,
 		title,
@@ -4689,7 +5132,7 @@
 }
 /** Validate the whole batch before returning any assets; the caller owns persistence. */
 async function importImages(files) {
-	if (files.length > LIMITS.shots) throw new Error("Choose up to 20 images at a time.");
+	if (files.length > LIMITS.shots) throw new Error("Choose up to 50 images at a time.");
 	for (const file of files) {
 		if (!file.size) throw new Error(`“${file.name}” is empty (0 bytes). Choose the original image or download it again.`);
 		if (file.size > LIMITS.assetBytes) throw new Error(`“${file.name}” is ${(file.size / 1024 / 1024).toFixed(1)} MB. The limit is 50 MB per image. Compress a copy or choose a smaller image.`);
@@ -5018,7 +5461,7 @@
 	if (!exportProfiles.some((p) => p.id === raw.profile)) throw new Error("Unknown export profile. Run hen profiles for supported IDs.");
 	if (raw.sourceLanguage !== void 0 && !isLanguage(raw.sourceLanguage)) throw new Error("Unsupported sourceLanguage. Run hen languages.");
 	if (raw.brandKit !== void 0) string(raw.brandKit, 4096, "brandKit");
-	if (!Array.isArray(raw.slides) || !raw.slides.length || raw.slides.length > LIMITS.shots) throw new Error("Provide 1–20 slides.");
+	if (!Array.isArray(raw.slides) || !raw.slides.length || raw.slides.length > LIMITS.shots) throw new Error("Provide 1–50 slides.");
 	raw.slides.forEach((value, index) => {
 		const at = `slides[${index}]`;
 		const slide = object(value, [
@@ -5335,13 +5778,31 @@
 		strokeWidth: Math.min(device.width, device.height) * .0015,
 		listening: false
 	}));
+	if (device.frame && device.homeButton) group.add(new Konva.Rect({
+		...device.homeButton,
+		cornerRadius: device.homeButton.radius,
+		fill: "#101114",
+		stroke: "#70747A",
+		strokeWidth: Math.min(device.width, device.height) * .002,
+		listening: false
+	}));
 	if (!camera || device.family === "card") return;
 	const cutout = device.camera;
 	const diameter = Math.min(cutout.width, cutout.height);
-	const island = device.family === "ios";
+	const island = device.family === "ios" && device.cameraKind !== "lens";
 	group.add(new Konva.Rect({
 		...cutout,
-		cornerRadius: cutout.radius,
+		cornerRadius: device.cameraKind === "notch" ? cutout.width > cutout.height ? [
+			0,
+			0,
+			cutout.radius,
+			cutout.radius
+		] : [
+			0,
+			cutout.radius,
+			cutout.radius,
+			0
+		] : cutout.radius,
 		fill: "#050608",
 		stroke: "#24262B",
 		strokeWidth: diameter * .035,
@@ -7004,6 +7465,38 @@
 		fill,
 		opacity
 	}));
+	if (style.template.startsWith("portfolio-")) {
+		const editorial = style.template === "portfolio-laptop-editorial";
+		rect({
+			x: x - 16,
+			y: y - 16,
+			width: w + 32,
+			height: ph + 32,
+			fill: style.backgroundEnd,
+			opacity: editorial ? .45 : .7,
+			cornerRadius: editorial ? 4 : 24
+		});
+		line([
+			80,
+			h * .85,
+			1e3,
+			h * .85
+		], .45, 1);
+		if (editorial) {
+			line([
+				76,
+				h * .235,
+				1004,
+				h * .235
+			], .65, 1.5);
+			line([
+				76,
+				h * .035,
+				1004,
+				h * .035
+			], .65, 2);
+		}
+	}
 	if (style.template === "prism") {
 		polygon([
 			x - 30,
@@ -8798,7 +9291,7 @@
 			if (empty && !options.emptyDeviceLabel) continue;
 			if (!empty && (!source?.complete || source.naturalWidth <= 0 || source.naturalHeight <= 0)) throw new Error("The screenshot has not finished loading.");
 			const imageWidth = source?.naturalWidth ?? 0, imageHeight = source?.naturalHeight ?? 0;
-			const device = deviceGeometry(style.device, slot.phone.width, style.frame, style.deviceOrientation);
+			const device = deviceGeometry(style.device, slot.phone.width, style.frame, style.deviceOrientation, project.exportProfile);
 			const phone = new Konva.Group({
 				x: slot.phone.x + device.width / 2 - cropOffset,
 				y: slot.phone.y + device.height / 2,
@@ -9012,7 +9505,7 @@
 	for (const slot of [shot, ...(shot.companions ?? []).map((d) => deviceShot(shot, `device:${d.id}`))]) {
 		if (slot.assetId !== asset.id) continue;
 		const style = resolveStyle(project, slot);
-		const screen = deviceGeometry(style.device, slot.phone.width, style.frame, style.deviceOrientation).screen;
+		const screen = deviceGeometry(style.device, slot.phone.width, style.frame, style.deviceOrientation, project.exportProfile).screen;
 		scales.push(fitImage(asset.width, asset.height, screen, style.fit).width / asset.width);
 	}
 	for (const owner of linkedShots(project, shot.id)) for (const overlay of owner.overlays ?? []) if (overlay.assetId === asset.id) scales.push(Math.max(overlay.width / asset.width, overlay.height / asset.height));
Full technical diff · 18 changed fields

changed /files/.claude-plugin~1plugin.json/sha256

BEFORE
"bb2f8bd86f949e5260ce40f1c3215672ba0f1e576284dd152053cca7ac90afaf"
AFTER
"b9cbe58f74b25ba251d3a0205c6b25dd41276d21348fa817ebd5dc9dfe92514e"

changed /files/.claude-plugin~1plugin.json/size

BEFORE
519
AFTER
543

removed /files/.codex-plugin~1assets~1composer-icon.png

BEFORE
{
  "sha256": "5abf911a83f89a2a655d94ec0bad78a3b9f633388159a9de6305485ee7d910ff",
  "size": 14032
}
AFTER
Field is absent

removed /files/.codex-plugin~1assets~1logo.png

BEFORE
{
  "sha256": "5abf911a83f89a2a655d94ec0bad78a3b9f633388159a9de6305485ee7d910ff",
  "size": 14032
}
AFTER
Field is absent

changed /files/.codex-plugin~1plugin.json/sha256

BEFORE
"54fda44ea82e66c14af4931176e733f86aa1d35937797e90826c4d06e230bb64"
AFTER
"871b37074b54c476c2370fc9f5bed0e940c53abc2b935292ccfc7631a5168a7c"

changed /files/.codex-plugin~1plugin.json/size

BEFORE
2697
AFTER
2487

changed /files/.cursor-plugin~1plugin.json/sha256

BEFORE
"37fd55422d25ec02990737b4245e26b286adf9f98d5a0983cc1b46b4fa755ae1"
AFTER
"3e26faf8d304d398c6611c4397a55bb67c80174e6049a5a6c9539d675a100554"

changed /files/.cursor-plugin~1plugin.json/size

BEFORE
539
AFTER
563

changed /files/PRIVACY.md/sha256

BEFORE
"0e53625b54bb91924cb0aa236c58d695fdeb49f25bade85622cb63c533cadf76"
AFTER
"7fdfe3f367a6f64a4630ec35b84ea3fbc6b6df62b1cc248e32c7821dd4db0eac"

changed /files/PRIVACY.md/size

BEFORE
1276
AFTER
3008

changed /files/README.md/sha256

BEFORE
"fbe184e28f873f40a6f4ef978024174ddb4b211e7de59da26e7c77dfd707b78a"
AFTER
"23b46e8bee6c114954740e6470e686061b445c001cf1d7a11d0386fb1c52f824"

changed /files/README.md/size

BEFORE
9001
AFTER
10388

changed /files/dist~1cli.mjs/sha256

BEFORE
"ba8ddc3e985f19209704059a02ededb3c2cb157a022c69510637b645d9dff1fa"
AFTER
"9cfc0af21abbdba3cb9d75a379e551fe2b9a73c76dd21f5ef346c7bd65363611"

changed /files/dist~1cli.mjs/size

BEFORE
248598
AFTER
261782

added /files/gemini-extension.json

BEFORE
Field was absent
AFTER
{
  "sha256": "8d620e9b087aa23f96e4e1282c96bcde3728bd8819d4b0b0987f84c5ef43eb8a",
  "size": 263
}

changed /files/package-lock.json/sha256

BEFORE
"0f476d2a402f45d6b4659ee0b4cc7328da1919ebdfbeac74db5e621d2bb2435e"
AFTER
"34a2c9ed9c957e477a88ddafb79c332d790109d59f9dcf16e0e2854c46711fd4"

changed /files/package.json/sha256

BEFORE
"5db7997168a45aae3609aa5b816b60da08859c9deb01b56387aa8fedb20ae1e6"
AFTER
"2fee52a495fae5a2953f766f039fab08f05038e3380e574a0081e56d14aa2b1c"

changed /files/plugin.json/sha256

BEFORE
"002f1a660c59ab18b2b0cb0bc816a99afb4a1ae47727d86eb063def1f3890754"
AFTER
"3a6883db6dec3a5adb47a0d11733a9e0d92123560449ad9f2ca3bb465734b2c2"
Full snapshot data
{
  "files": {
    ".claude-plugin/plugin.json": {
      "sha256": "b9cbe58f74b25ba251d3a0205c6b25dd41276d21348fa817ebd5dc9dfe92514e",
      "size": 543
    },
    ".codex-plugin/plugin.json": {
      "sha256": "871b37074b54c476c2370fc9f5bed0e940c53abc2b935292ccfc7631a5168a7c",
      "size": 2487
    },
    ".cursor-plugin/plugin.json": {
      "sha256": "3e26faf8d304d398c6611c4397a55bb67c80174e6049a5a6c9539d675a100554",
      "size": 563
    },
    "LICENSE": {
      "sha256": "017fa967a6f8eb914b72104be8f53001f7c40326dfb2beb99df4855c8d7eb3df",
      "size": 1064
    },
    "PRIVACY.md": {
      "sha256": "7fdfe3f367a6f64a4630ec35b84ea3fbc6b6df62b1cc248e32c7821dd4db0eac",
      "size": 3008
    },
    "README.md": {
      "sha256": "23b46e8bee6c114954740e6470e686061b445c001cf1d7a11d0386fb1c52f824",
      "size": 10388
    },
    "THIRD_PARTY_NOTICES.md": {
      "sha256": "eba57deade7af4cac0cb5e90f9c1806992b1cee93033fcdf0e2997e3b6ff40fd",
      "size": 1278
    },
    "assets/fonts/Fraunces-Semibold.ttf": {
      "sha256": "1390d372c2edc7881191bef9fb673ed8ec4d2bf143a2f68b63c01094794307ac",
      "size": 72952
    },
    "assets/fonts/Manrope.ttf": {
      "sha256": "d0639be45d0af36e798172419d7bd173c4bd4f29e2b76cbb69db1d11bf8b0a40",
      "size": 165420
    },
    "assets/fonts/OFL-Fraunces.txt": {
      "sha256": "bdf4c22802eaf804f998195871c6b8938aac2ac14b2d78a8bd66a6f1eced833b",
      "size": 4391
    },
    "assets/fonts/OFL.txt": {
      "sha256": "e01b637272e0cbdfb240184dd98ea5cc671556d9894dae2668d92ab2c906787c",
      "size": 4384
    },
    "assets/icon.png": {
      "sha256": "5abf911a83f89a2a655d94ec0bad78a3b9f633388159a9de6305485ee7d910ff",
      "size": 14032
    },
    "dist/cli.mjs": {
      "sha256": "9cfc0af21abbdba3cb9d75a379e551fe2b9a73c76dd21f5ef346c7bd65363611",
      "size": 261782
    },
    "gemini-extension.json": {
      "sha256": "8d620e9b087aa23f96e4e1282c96bcde3728bd8819d4b0b0987f84c5ef43eb8a",
      "size": 263
    },
    "package-lock.json": {
      "sha256": "34a2c9ed9c957e477a88ddafb79c332d790109d59f9dcf16e0e2854c46711fd4",
      "size": 24103
    },
    "package.json": {
      "sha256": "2fee52a495fae5a2953f766f039fab08f05038e3380e574a0081e56d14aa2b1c",
      "size": 291
    },
    "plugin.json": {
      "sha256": "3a6883db6dec3a5adb47a0d11733a9e0d92123560449ad9f2ca3bb465734b2c2",
      "size": 618
    },
    "scripts/hen.mjs": {
      "sha256": "635ad1625a2ae6506d53eeef9e172287e3cfcf3a712b8ab694dcb63d6857c749",
      "size": 762
    },
    "scripts/setup.mjs": {
      "sha256": "540a8b07a36757453607733a63a5b4d383490543b505daa0c1abe99401f38903",
      "size": 2016
    },
    "skills/create-screenshots/SKILL.md": {
      "sha256": "62c265404fa06509952aadfe7fa366b222712cc7c34ddf93e8fc76fbd6bc21dc",
      "size": 4530
    },
    "skills/create-screenshots/references/design-spec.md": {
      "sha256": "e887973db4a5a59f670dab59fa61d9f382522c0ca990d12cd5c06e481b7a9867",
      "size": 6862
    }
  }
}

SHA-256 of public snapshot: 625bfa35ec2afe5578173e3177fa6e33f7bb9fe699855b1cfadc6348c5e0d7a6