diff --git a/docs/authors/writing-tutorials.md b/docs/authors/writing-tutorials.md
index f368b6d76..d58236c92 100644
--- a/docs/authors/writing-tutorials.md
+++ b/docs/authors/writing-tutorials.md
@@ -78,7 +78,7 @@ author_profile: https://github.com/janedoe
| `auto_validation` | No | Set `true` if the tutorial has quiz rules in the `-Contribution` repo. |
| `author_name`, `author_profile` | Recommended | Shown on the tutorial page. |
| `video` | No | Optional intro video shown as a styled player at the top of the steps. See [§3.3 Adding a video](#_3-3-adding-a-video). |
-| `variables` | No | Array of per-reader values (e.g. a workshop group number) the reader fills in once and the platform substitutes live throughout the tutorial. See [§3.7 Personalize with variables](#_3-7-personalize-with-variables). |
+| `variables` | No | Array of substituted values — either per-reader (e.g. a workshop group number the reader fills in) or author-fixed (`value:`, optionally `display: false` to hide). The platform substitutes them live throughout the tutorial. See [§3.7 Personalize with variables](#_3-7-personalize-with-variables). |
A canonical taxonomy of allowed `primary_tag` and `tags` values is maintained in the platform's `Tags` entity. To add a new tag or category, see [Center Admin](center-admin.md) § "Import a new tag".
@@ -356,6 +356,22 @@ Optionally pre-fill the value from the reader's identity with a `default` regex
pattern: 'da164-(\d+)@' # capture group 1 → default value
```
+#### Author-fixed values and hidden variables
+
+Sometimes the value is the **same for every reader** but repeated throughout the tutorial — a product name, a release version, a service URL. Set it **once** with `value:` and reference `{{var:}}` everywhere; change it in one place and it updates across the whole tutorial. Add `display: false` so the reader never sees an input for it:
+
+```yaml
+variables:
+ - name: productVersion
+ type: string
+ value: "MyApp 2.0" # author-fixed literal — substituted everywhere
+ display: false # hidden: no input shown to the reader
+```
+
+- `display` is **optional and defaults to `true`** (shown). Only `display: false` hides the input. A hidden variable needs no `label`.
+- `value:` overrides a `default:` regex. Precedence for the substituted value is: **`value` → `default` (regex) → what the reader types → empty**.
+- A displayed variable (`display: true` or omitted) with a `value:` is **pre-filled** with that value but the reader can still edit it.
+
Key behaviors:
- **Build-enforced.** A malformed `variables:` block, an invalid `type`, a `default.from` off the allowlist `{email, id, givenName, familyName}`, an uncompilable `default.pattern`, or a `{{var:}}` token referencing an undeclared name is a **hard build failure** — it can never reach readers. (A declared-but-unused variable is a soft warning.)
diff --git a/docs/superpowers/plans/2026-10-20-hidden-tutorial-variables.md b/docs/superpowers/plans/2026-10-20-hidden-tutorial-variables.md
new file mode 100644
index 000000000..7e38bad40
--- /dev/null
+++ b/docs/superpowers/plans/2026-10-20-hidden-tutorial-variables.md
@@ -0,0 +1,168 @@
+# Hidden & Author-Fixed Tutorial Variables Implementation Plan
+
+> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
+
+**Goal:** Let tutorial authors declare a variable with a fixed literal `value:` and/or hide it from the reader-facing "Your values" panel via `display: false`, while it still substitutes throughout the tutorial body.
+
+**Architecture:** Extend the existing #2811 tutorial-variables feature with two optional frontmatter fields — `value?: string` (author-fixed literal) and `display?: boolean` (default `true`). Build-time: validate + carry both through compose → render-frontmatter → Hugo frontmatter → island JSON config (no change to token-wrapping; hidden vars still emit `[data-tut-var]` spans/sentinels and are substituted client-side). Runtime (Vue island): render an input only for variables where `display !== false`; seed **every** variable's value with precedence `value` > identity `default` (regex) > persisted/stored > empty, and `applyValue()` so hidden/fixed vars substitute even with no input field.
+
+**Tech Stack:** TypeScript (Node build scripts, Vitest), Vue 3 SFC island, Hugo Go templates.
+
+**Spec:** GitHub issue [#2852](https://github.com/sap-tutorials/tutorials-ims/issues/2852) — "Variables feature - no display to user." (no separate design doc; this plan is the design of record)
+
+## Global Constraints
+
+- Default MUST be to display (`display` absent or `true` → input shown). Copied from issue: "Default is to display them."
+- A literal `value:` MUST override a regex `default:` when both are present. Precedence: `value` > `default` > stored/reader input > empty. (User decision, this conversation.)
+- No new npm dependencies.
+- Backward compatible: a tutorial whose variables declare neither new field behaves exactly as today (input shown, no literal value).
+- The client-side `TutorialVariable` interface in `hugo-apps/src/tutorial-variables/substitution.ts` MUST stay field-compatible with the build-side one in `scripts/parsers/variables.ts` (the island receives these objects verbatim as JSON).
+- XSS: a literal `value` reaches the DOM only via `applyValue()` which sets `.textContent` (never innerHTML) — keep it that way; do not interpolate `value` into any HTML string.
+
+## Review Focus
+
+- **`value` with HTML/script-ish characters** (e.g. `value: "x"`) — must render as literal text, never parsed HTML. Pinned in Task 4 island test (`applyValue` already uses textContent; add a test asserting a `value` with `<` survives as text).
+- **`display: false` with a non-boolean (`display: "false"` string)** — validator must reject non-boolean `display`. Pinned in Task 1.
+- **Hidden variable (`display:false`) with no `value` and no `default`** — substitutes to empty string, must not throw or leave the literal token/sentinel visible. Pinned in Task 4.
+- **`label` omitted on a `display:false` variable** — allowed (no visible label needed); but omitted on a displayed variable must still throw. Pinned in Task 1.
+- **All variables hidden** — the "Your values" panel must not render an empty shell (no heading/hint with zero inputs). Pinned in Task 4.
+
+---
+
+### Task 1: Validate `value` and `display` in the build-time parser
+
+**Files:**
+- Modify: `scripts/parsers/variables.ts` (interface ~31-42; validation loop ~122-214)
+- Test: `scripts/parsers/__tests__/variables.test.ts`
+
+**Interfaces:**
+- Produces: `TutorialVariable` gains `value?: string` and `display?: boolean`. `label` becomes optional in the type (`label?: string`) but `validateVariables` still throws when `label` is missing/empty AND `display !== false`.
+
+- [ ] **Step 1: Write failing tests** in `variables.test.ts`:
+ - `accepts a variable with a literal value` — `[{name:'prod',label:'P',type:'string',value:'MyApp 2.0'}]` → result[0].value === 'MyApp 2.0'.
+ - `accepts display:false and omits it as not-shown` — `[{name:'prod',type:'string',value:'x',display:false}]` → result[0].display === false (and no throw despite missing label).
+ - `throws when display is not a boolean` — `[{name:'prod',label:'P',type:'string',display:'false'}]` → throws `VariablesValidationError` matching `/display/`.
+ - `throws when value is not a string` — `[{name:'prod',label:'P',type:'string',value:123}]` → throws matching `/value/`.
+ - `throws when label missing on a displayed variable` — `[{name:'prod',type:'string'}]` → throws matching `/label/` (existing behavior preserved for the default display=true case).
+ - `defaults display to shown when absent` — `[{name:'g',label:'G',type:'string'}]` → `result[0].display` is `undefined` (absence = shown; island treats `!== false` as shown).
+
+- [ ] **Step 2: Run to verify fail** — `npx vitest run scripts/parsers/__tests__/variables.test.ts` → FAIL (value/display not carried; label-missing no longer throws for hidden).
+
+- [ ] **Step 3: Implement** in `variables.ts`:
+ - Add `value?: string` and `display?: boolean` to the interface; change `label` to `label?: string`.
+ - In the loop: read `display` first; if present and not `typeof === 'boolean'` → throw `tutorial variables "": display must be a boolean`.
+ - Label check: throw missing-label only when `display !== false`. (Keep exact existing message for the displayed case.)
+ - Read `value`: if present and `typeof !== 'string'` → throw `tutorial variables "": value must be a string`.
+ - On the validated object: `if (typeof raw['value'] === 'string') validated.value = raw['value']`; `if (typeof raw['display'] === 'boolean') validated.display = raw['display']`.
+
+- [ ] **Step 4: Run to verify pass** — same command → PASS.
+
+- [ ] **Step 5: Commit** — `feat(#2852): validate value + display fields on tutorial variables`.
+
+---
+
+### Task 2: Carry `value`/`display` through compose + source-frontmatter type
+
+**Files:**
+- Modify: `scripts/parsers/types.ts` (`TutorialFrontmatter.variables` inline schema ~23-31)
+- Test: `scripts/parsers/__tests__/variables.test.ts` (the `composeTutorial with variables` integration describe block)
+
+**Interfaces:**
+- Consumes: Task 1 `TutorialVariable`.
+- Produces: `TutorialFrontmatter.variables[]` inline type gains `value?: string` and `display?: boolean` and `label?` optional. `composeTutorial` already spreads the validated array into `ComposeResult.variables` (no code change there) — this task proves the new fields survive compose.
+
+- [ ] **Step 1: Write failing test** — in the `composeTutorial with variables` block, add `carries value and display through compose`: a v2 tutorial whose frontmatter declares `variables: [{name:'prod',type:'string',value:'MyApp 2.0',display:false}]` and body uses `{{var:prod}}`; assert `result.variables[0].value === 'MyApp 2.0'` and `result.variables[0].display === false`, and that `result.body` contains a `data-tut-var="prod"` span (substitution still wired).
+
+- [ ] **Step 2: Run to verify fail** — `npx vitest run scripts/parsers/__tests__/variables.test.ts` → FAIL (type rejects fields / assertion).
+
+- [ ] **Step 3: Implement** — in `types.ts`, add `value?: string`, `display?: boolean` to the `variables?` array element and make `label?` optional. (No `compose.ts` logic change — it passes the validated array through.)
+
+- [ ] **Step 4: Run to verify pass** — PASS.
+
+- [ ] **Step 5: Commit** — `feat(#2852): thread value/display through compose + source type`.
+
+---
+
+### Task 3: Emit `value`/`display` into Hugo frontmatter
+
+**Files:**
+- Modify: `scripts/parsers/render-frontmatter.ts` (type import side ~75-80; emit ~198)
+- Test: add `scripts/parsers/__tests__/render-frontmatter-variables.test.ts` (mirrors the existing `render-frontmatter-*.test.ts` files; `renderHugoFrontmatter(args): string` returns the YAML frontmatter string, assert on substrings)
+
+**Interfaces:**
+- Consumes: Task 1 `TutorialVariable` (already imported at `render-frontmatter.ts:8`).
+- Produces: Hugo content frontmatter `variables:` array includes `value`/`display` keys verbatim when set. Downstream Hugo template (`u1-object-page.html:510`) jsonifies `.Params.variables` unchanged — no template change needed because it serializes the whole array.
+
+- [ ] **Step 1: Write failing test** — in a new `render-frontmatter-variables.test.ts`, `emits value and display in frontmatter when set`: call `renderHugoFrontmatter(args)` (build `args` the way the sibling `render-frontmatter-*.test.ts` files do) with `variables: [{name:'prod',type:'string',value:'MyApp 2.0',display:false}]`; assert the returned string contains `value: MyApp 2.0` and `display: false`.
+
+- [ ] **Step 2: Run to verify fail** — `npx vitest run scripts/parsers/__tests__/render-frontmatter*.test.ts` → FAIL.
+
+- [ ] **Step 3: Implement** — the current emit `if (variables && variables.length > 0) fm.variables = variables` already copies all fields verbatim. Confirm `variables` is typed to include the new fields (it uses `TutorialVariable`); if the local destructured type is a narrower inline shape, widen it. Likely a no-op beyond confirming the type carries through — add fields only if a narrower type blocks them.
+
+- [ ] **Step 4: Run to verify pass** — PASS.
+
+- [ ] **Step 5: Commit** — `feat(#2852): emit value/display in tutorial Hugo frontmatter`.
+
+---
+
+### Task 4: Island — hide non-displayed inputs, seed value precedence, substitute all
+
+**Files:**
+- Modify: `hugo-apps/src/tutorial-variables/substitution.ts` (`TutorialVariable` interface ~17-23; add `resolveInitial` helper)
+- Modify: `hugo-apps/src/tutorial-variables/TutorialVariables.vue` (template `v-if` + `v-for`; `onMounted`)
+- Test: `hugo-apps/src/tutorial-variables/__tests__/substitution.test.ts`
+
+**Interfaces:**
+- Consumes: island config objects now carrying `value?: string`, `display?: boolean`.
+- Produces: `resolveInitial(v: TutorialVariable, identity: Record|null): string | null` — returns `v.value` if a non-empty string; else `resolveDefault(v, identity)`; else `null`. (`value` overrides `default`.)
+
+- [ ] **Step 1: Write failing tests** in `substitution.test.ts`:
+ - Add `value?: string` / `display?: boolean` to the local test `TutorialVariable` usages as needed.
+ - `resolveInitial prefers literal value over default` — var with both `value:'FIXED'` and a matching `default` + identity → returns `'FIXED'`.
+ - `resolveInitial falls back to default when no value` — var with only `default`, matching identity → returns the regex capture.
+ - `resolveInitial returns null when neither value nor resolvable default` — returns `null`.
+ - `applyValue renders a value containing < as literal text` — `applyValue('x','hi')` on a `[data-tut-var="x"]` span → `span.textContent === 'hi'` and `span.children.length === 0`.
+
+- [ ] **Step 2: Run to verify fail** — `npx vitest run hugo-apps/src/tutorial-variables/__tests__/substitution.test.ts` → FAIL (`resolveInitial` undefined).
+
+- [ ] **Step 3: Implement**:
+ - `substitution.ts`: add `value?: string` and `display?: boolean` to `TutorialVariable` (keep `label?` optional to match build side). Add and export `resolveInitial`.
+ - `TutorialVariables.vue`:
+ - Add `import { resolveInitial } from './substitution'`.
+ - Template: compute `const shown = computed(() => props.variables.filter(v => v.display !== false))`. Panel ``; `v-for="v in shown"`.
+ - `onMounted`: iterate **all** `props.variables` (not just shown). For each: initialise `fieldValues[v.name]=''`. Restore persisted (existing). Then if field still empty, set from `resolveInitial(v, identity)` when non-null and `applyValue`. A literal `value` must apply even for hidden vars — so order: persisted (only meaningful for shown/editable) then `resolveInitial`. Since `value` should win over a stale persisted entry for *fixed* vars, apply `value` unconditionally for `display === false` (hidden = author-owned, not reader-owned): if `v.display === false && typeof v.value === 'string'` set `fieldValues[v.name]=v.value` and `applyValue` regardless of stored.
+ - Leave `onInput`/storage untouched (hidden vars have no input, so onInput never fires for them).
+
+- [ ] **Step 4: Run to verify pass** — PASS.
+
+- [ ] **Step 5: Commit** — `feat(#2852): island hides non-displayed vars, value overrides default`.
+
+---
+
+### Task 5: Documentation + example + full island build/manifest
+
+**Files:**
+- Modify: `scripts/parsers/variables.ts` top doc-comment (document `value`/`display`)
+- Modify: relevant reference doc if one documents variable frontmatter (grep `docs/` for the variables schema; update the schema table if found)
+- Build: rebuild the island bundle + island manifest so the shipped JS includes the island changes
+
+**Interfaces:**
+- Consumes: Tasks 1-4.
+
+- [ ] **Step 1:** Update the doc-comment block in `variables.ts` to show `value:` and `display:` with a one-line each explanation and the precedence note.
+
+- [ ] **Step 2:** `grep -rn "display\|value\|variables:" docs/ | grep -i variable` — if a schema doc exists for tutorial variables, add the two fields + precedence + default-display note. If none, skip (note in commit).
+
+- [ ] **Step 3:** Rebuild island + manifest so the bundle isn't stale (gotcha: `ignore-scripts=true` means `postbuild:apps` won't auto-run). Run the island build the repo uses (`npm run build:apps` or the vite build for hugo-apps) and the island-manifest step, then confirm `hugo/data/island_manifest.json` points at the new `tutorial-variables-*.js` and the file exists under `hugo/static/js/`.
+
+- [ ] **Step 4: Run full variable test suites** — `npx vitest run scripts/parsers/__tests__/variables.test.ts hugo-apps/src/tutorial-variables/__tests__/` → all PASS.
+
+- [ ] **Step 5: Commit** — `docs(#2852): document value/display + rebuild variables island`.
+
+---
+
+## Notes for the executor
+
+- The canonical demo tutorial `hugo/content/tutorials/use-variables-in-tutorials.md` is **generated** from the `sap-tutorials` source repo at build time — do NOT hand-edit it. Updating the live demo tutorial to exercise a hidden/fixed variable is a separate source-repo change (out of scope for this PR; mention in the PR body as a follow-up).
+- PR targets **DEV**, not `main`. Branch is already reset onto `origin/DEV`.
+- Keep each task's commit scoped; the whole-branch review runs after Task 5.
diff --git a/hugo-apps/src/tutorial-variables/TutorialVariables.vue b/hugo-apps/src/tutorial-variables/TutorialVariables.vue
index f147e9c15..f2dca9297 100644
--- a/hugo-apps/src/tutorial-variables/TutorialVariables.vue
+++ b/hugo-apps/src/tutorial-variables/TutorialVariables.vue
@@ -11,7 +11,7 @@
-->