@zitadel/components 1.0.0-alpha.22 → 1.0.0-alpha.23

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,5 +1,5 @@
1
- import { C as focusVisibleStyles, E as emit, S as __decorate, T as t, d as hookName, w as baseHostStyles } from "./atoms-Boaxh79f.mjs";
2
- import { i as tokensCss, t as THEME_SELECTORS } from "./tokens-T4N9VJz3.mjs";
1
+ import { C as focusVisibleStyles, E as emit, S as __decorate, T as t, d as hookName, w as baseHostStyles } from "./atoms-DbcPXAO0.mjs";
2
+ import { i as tokensCss, r as tokens, t as THEME_SELECTORS } from "./tokens-T4N9VJz3.mjs";
3
3
  import { manifestRegistry } from "./manifests.mjs";
4
4
  import { t as default_default } from "./default-Dnjq9iBt.mjs";
5
5
  import { LitElement, css, html, nothing } from "lit";
@@ -331,18 +331,28 @@ function validateBranding(input, context = {}) {
331
331
  out.layout = CreateFlow201BrandingLayout.centered;
332
332
  }
333
333
  out.logo_url = sanitiseUrl(out.logo_url, "logo_url", issues, { allowed: allowLoopbackHttp });
334
- out.font_url = sanitiseUrl(out.font_url, "font_url", issues);
334
+ if (out.typography?.font_url) out.typography = {
335
+ ...out.typography,
336
+ font_url: sanitiseUrl(out.typography.font_url, "typography.font_url", issues)
337
+ };
335
338
  out.hero_url = sanitiseUrl(out.hero_url, "hero_url", issues, { allowed: allowLoopbackHttp });
336
- if (out.assets) out.assets = {
337
- logo_dark: sanitiseUrl(out.assets.logo_dark, "assets.logo_dark", issues),
338
- favicon: sanitiseUrl(out.assets.favicon, "assets.favicon", issues),
339
- background_image: sanitiseUrl(out.assets.background_image, "assets.background_image", issues)
339
+ if (out.theme?.light || out.theme?.dark) out.theme = {
340
+ ...out.theme,
341
+ ...sanitiseSideLogo(out.theme.light, "light", issues, allowLoopbackHttp),
342
+ ...sanitiseSideLogo(out.theme.dark, "dark", issues, allowLoopbackHttp)
340
343
  };
341
344
  return {
342
345
  branding: out,
343
346
  issues
344
347
  };
345
348
  }
349
+ function sanitiseSideLogo(side, name, issues, allowLoopbackHttp) {
350
+ if (!side) return {};
351
+ return { [name]: {
352
+ ...side,
353
+ logo_url: sanitiseUrl(side.logo_url, `theme.${name}.logo_url`, issues, { allowed: allowLoopbackHttp })
354
+ } };
355
+ }
346
356
  function sanitiseUrl(value, field, issues, loopbackHttp) {
347
357
  if (value == null || value === "") return;
348
358
  try {
@@ -360,6 +370,34 @@ function sanitiseUrl(value, field, issues, loopbackHttp) {
360
370
  }
361
371
  }
362
372
  //#endregion
373
+ //#region src/orchestrator/branding.ts
374
+ /**
375
+ * Which sides the revision actually publishes. A side is published when the
376
+ * revision names it at all — an empty side object still says "this surface is
377
+ * mine", it just takes the maintained defaults for every key.
378
+ */
379
+ function publishedSides(branding) {
380
+ const theme = branding?.theme;
381
+ const sides = [];
382
+ if (theme?.light) sides.push("light");
383
+ if (theme?.dark) sides.push("dark");
384
+ return sides;
385
+ }
386
+ /**
387
+ * The mark for the resolved surface. A logo is pixels and is never recoloured,
388
+ * so a side without its own file shows no mark rather than borrowing the other
389
+ * side's. The legacy top-level `logo_url` is a single-mark fallback and applies
390
+ * only when neither side names one.
391
+ */
392
+ function resolveLogoUrl(branding, theme) {
393
+ if (!branding) return void 0;
394
+ const sides = branding.theme;
395
+ const sideLogo = theme === "light" ? sides?.light?.logo_url : sides?.dark?.logo_url;
396
+ if (sideLogo) return sideLogo;
397
+ if (sides?.light?.logo_url || sides?.dark?.logo_url) return void 0;
398
+ return branding.logo_url;
399
+ }
400
+ //#endregion
363
401
  //#region src/orchestrator/exportparts.ts
364
402
  /**
365
403
  * Forwards atom-internal CSS Shadow Parts through the orchestrator boundary.
@@ -864,7 +902,7 @@ const en = {
864
902
  "error.password_required": "Please enter a password",
865
903
  "error.password_incorrect": "Wrong email or password.",
866
904
  "error.email_exists": "An account with this email already exists.",
867
- /** Figma sign-in error `6602:180268` — inline on password field. */
905
+ /** Sign-in error, inline on the password field. */
868
906
  "error.invalid_credentials": "Wrong email or password.",
869
907
  "error.required": "This field is required.",
870
908
  "error.field_required": "{0} is required.",
@@ -1187,16 +1225,17 @@ var zitadelNextGen_exports = /* @__PURE__ */ __exportAll({
1187
1225
  createFlowDefinition: () => createFlowDefinition,
1188
1226
  createGrant: () => createGrant,
1189
1227
  createHandoff: () => createHandoff,
1228
+ createIdp: () => createIdp,
1190
1229
  createProject: () => createProject,
1191
1230
  createRelease: () => createRelease,
1192
1231
  createSchema: () => createSchema,
1193
1232
  createSession: () => createSession,
1194
1233
  createTeam: () => createTeam,
1195
1234
  createUser: () => createUser,
1196
- deleteFlowDefinition: () => deleteFlowDefinition,
1197
1235
  deleteGrant: () => deleteGrant,
1198
1236
  deleteTeam: () => deleteTeam,
1199
1237
  deleteUserByID: () => deleteUserByID,
1238
+ deleteVariable: () => deleteVariable,
1200
1239
  exchangeHandoff: () => exchangeHandoff,
1201
1240
  finishUserPasskeyRegistration: () => finishUserPasskeyRegistration,
1202
1241
  getAuthAttempt: () => getAuthAttempt,
@@ -1211,16 +1250,17 @@ var zitadelNextGen_exports = /* @__PURE__ */ __exportAll({
1211
1250
  getCreateFlowUrl: () => getCreateFlowUrl,
1212
1251
  getCreateGrantUrl: () => getCreateGrantUrl,
1213
1252
  getCreateHandoffUrl: () => getCreateHandoffUrl,
1253
+ getCreateIdpUrl: () => getCreateIdpUrl,
1214
1254
  getCreateProjectUrl: () => getCreateProjectUrl,
1215
1255
  getCreateReleaseUrl: () => getCreateReleaseUrl,
1216
1256
  getCreateSchemaUrl: () => getCreateSchemaUrl,
1217
1257
  getCreateSessionUrl: () => getCreateSessionUrl,
1218
1258
  getCreateTeamUrl: () => getCreateTeamUrl,
1219
1259
  getCreateUserUrl: () => getCreateUserUrl,
1220
- getDeleteFlowDefinitionUrl: () => getDeleteFlowDefinitionUrl,
1221
1260
  getDeleteGrantUrl: () => getDeleteGrantUrl,
1222
1261
  getDeleteTeamUrl: () => getDeleteTeamUrl,
1223
1262
  getDeleteUserByIDUrl: () => getDeleteUserByIDUrl,
1263
+ getDeleteVariableUrl: () => getDeleteVariableUrl,
1224
1264
  getEnvironmentByName: () => getEnvironmentByName,
1225
1265
  getEvent: () => getEvent,
1226
1266
  getExchangeHandoffUrl: () => getExchangeHandoffUrl,
@@ -1237,6 +1277,8 @@ var zitadelNextGen_exports = /* @__PURE__ */ __exportAll({
1237
1277
  getGetFlowStepUrl: () => getGetFlowStepUrl,
1238
1278
  getGetGrantUrl: () => getGetGrantUrl,
1239
1279
  getGetHealthUrl: () => getGetHealthUrl,
1280
+ getGetIdpByIdUrl: () => getGetIdpByIdUrl,
1281
+ getGetIdpRevisionByIdUrl: () => getGetIdpRevisionByIdUrl,
1240
1282
  getGetLiveUrl: () => getGetLiveUrl,
1241
1283
  getGetMySessionUrl: () => getGetMySessionUrl,
1242
1284
  getGetMyUserUrl: () => getGetMyUserUrl,
@@ -1247,14 +1289,20 @@ var zitadelNextGen_exports = /* @__PURE__ */ __exportAll({
1247
1289
  getGetSessionUrl: () => getGetSessionUrl,
1248
1290
  getGetTeamUrl: () => getGetTeamUrl,
1249
1291
  getGetUserByIDUrl: () => getGetUserByIDUrl,
1292
+ getGetVariableUrl: () => getGetVariableUrl,
1293
+ getGetVariablesUrl: () => getGetVariablesUrl,
1250
1294
  getGrant: () => getGrant,
1251
1295
  getHealth: () => getHealth,
1296
+ getIdpById: () => getIdpById,
1297
+ getIdpRevisionById: () => getIdpRevisionById,
1252
1298
  getInitClaimUrl: () => getInitClaimUrl,
1253
1299
  getIssueChallengeUrl: () => getIssueChallengeUrl,
1254
1300
  getListBrandingUrl: () => getListBrandingUrl,
1255
1301
  getListEnvironmentsUrl: () => getListEnvironmentsUrl,
1256
1302
  getListEventsUrl: () => getListEventsUrl,
1257
1303
  getListFlowDefinitionsUrl: () => getListFlowDefinitionsUrl,
1304
+ getListIdpRevisionsUrl: () => getListIdpRevisionsUrl,
1305
+ getListMyProjectsUrl: () => getListMyProjectsUrl,
1258
1306
  getListReleasesUrl: () => getListReleasesUrl,
1259
1307
  getListSchemasUrl: () => getListSchemasUrl,
1260
1308
  getListUserPasskeysUrl: () => getListUserPasskeysUrl,
@@ -1262,9 +1310,12 @@ var zitadelNextGen_exports = /* @__PURE__ */ __exportAll({
1262
1310
  getLive: () => getLive,
1263
1311
  getMySession: () => getMySession,
1264
1312
  getMyUser: () => getMyUser,
1313
+ getPatchMyUserUrl: () => getPatchMyUserUrl,
1265
1314
  getPatchProjectUrl: () => getPatchProjectUrl,
1315
+ getPatchUserByIDUrl: () => getPatchUserByIDUrl,
1266
1316
  getProject: () => getProject,
1267
1317
  getQueryGrantsUrl: () => getQueryGrantsUrl,
1318
+ getQueryIdpsUrl: () => getQueryIdpsUrl,
1268
1319
  getQueryProjectsUrl: () => getQueryProjectsUrl,
1269
1320
  getQuerySessionsUrl: () => getQuerySessionsUrl,
1270
1321
  getQueryTeamsUrl: () => getQueryTeamsUrl,
@@ -1278,9 +1329,11 @@ var zitadelNextGen_exports = /* @__PURE__ */ __exportAll({
1278
1329
  getSetUserPasswordUrl: () => getSetUserPasswordUrl,
1279
1330
  getSubmitFlowStepUrl: () => getSubmitFlowStepUrl,
1280
1331
  getTeam: () => getTeam,
1281
- getUpdateFlowDefinitionUrl: () => getUpdateFlowDefinitionUrl,
1282
1332
  getUpdateTeamUrl: () => getUpdateTeamUrl,
1333
+ getUpdateVariablesUrl: () => getUpdateVariablesUrl,
1283
1334
  getUserByID: () => getUserByID,
1335
+ getVariable: () => getVariable,
1336
+ getVariables: () => getVariables,
1284
1337
  getVerifyChallengeProofUrl: () => getVerifyChallengeProofUrl,
1285
1338
  initClaim: () => initClaim,
1286
1339
  issueChallenge: () => issueChallenge,
@@ -1288,12 +1341,17 @@ var zitadelNextGen_exports = /* @__PURE__ */ __exportAll({
1288
1341
  listEnvironments: () => listEnvironments,
1289
1342
  listEvents: () => listEvents,
1290
1343
  listFlowDefinitions: () => listFlowDefinitions,
1344
+ listIdpRevisions: () => listIdpRevisions,
1345
+ listMyProjects: () => listMyProjects,
1291
1346
  listReleases: () => listReleases,
1292
1347
  listSchemas: () => listSchemas,
1293
1348
  listUserPasskeys: () => listUserPasskeys,
1294
1349
  listUserTeams: () => listUserTeams,
1350
+ patchMyUser: () => patchMyUser,
1295
1351
  patchProject: () => patchProject,
1352
+ patchUserByID: () => patchUserByID,
1296
1353
  queryGrants: () => queryGrants,
1354
+ queryIdps: () => queryIdps,
1297
1355
  queryProjects: () => queryProjects,
1298
1356
  querySessions: () => querySessions,
1299
1357
  queryTeams: () => queryTeams,
@@ -1302,8 +1360,8 @@ var zitadelNextGen_exports = /* @__PURE__ */ __exportAll({
1302
1360
  revokeSession: () => revokeSession,
1303
1361
  setUserPassword: () => setUserPassword,
1304
1362
  submitFlowStep: () => submitFlowStep,
1305
- updateFlowDefinition: () => updateFlowDefinition,
1306
1363
  updateTeam: () => updateTeam,
1364
+ updateVariables: () => updateVariables,
1307
1365
  verifyChallengeProof: () => verifyChallengeProof
1308
1366
  });
1309
1367
  const getGetHealthUrl = () => {
@@ -1373,9 +1431,14 @@ const getQueryUsersUrl = () => {
1373
1431
  /**
1374
1432
  * Returns the users of a project, paginated with a cursor.
1375
1433
 
1376
- The project comes from the credential, not from a parameter: the operation
1377
- is bound to the token's own project by construction. This is why it takes
1378
- no `project_id`, unlike the other query endpoints.
1434
+ The project comes from the credential, not from a parameter: the
1435
+ operation is bound to the credential's home project by construction
1436
+ (oauth2 secret or user-bound session). This is why it takes no
1437
+ `project_id`, unlike the other query endpoints.
1438
+
1439
+ Accepts either a project secret (`oauth2`) or a user-bound Console
1440
+ session cookie (`nextgenSession`). CSRF/Origin for cookie mutations
1441
+ is a follow-up (#1140).
1379
1442
 
1380
1443
  * @summary Query users
1381
1444
  */
@@ -1407,6 +1470,28 @@ const getUserByID = async (userId, params, options) => {
1407
1470
  method: "GET"
1408
1471
  });
1409
1472
  };
1473
+ const getPatchUserByIDUrl = (userId) => {
1474
+ return `${getProxyPath()}/users/${userId}`;
1475
+ };
1476
+ /**
1477
+ * Partially updates a user's schema-defined attributes, and optionally
1478
+ moves the user to another registered schema. The merged result is
1479
+ validated against the schema before commit. Concurrent writes are
1480
+ last-write-wins.
1481
+
1482
+ * @summary Update user by ID
1483
+ */
1484
+ const patchUserByID = async (userId, patchUserByIDBody, options) => {
1485
+ return customFetch(getPatchUserByIDUrl(userId), {
1486
+ ...options,
1487
+ method: "PATCH",
1488
+ headers: {
1489
+ "Content-Type": "application/json",
1490
+ ...options?.headers
1491
+ },
1492
+ body: JSON.stringify(patchUserByIDBody)
1493
+ });
1494
+ };
1410
1495
  const getDeleteUserByIDUrl = (userId) => {
1411
1496
  return `${getProxyPath()}/users/${userId}`;
1412
1497
  };
@@ -1537,6 +1622,52 @@ const getMyUser = async (options) => {
1537
1622
  method: "GET"
1538
1623
  });
1539
1624
  };
1625
+ const getPatchMyUserUrl = () => {
1626
+ return `${getProxyPath()}/users/me`;
1627
+ };
1628
+ /**
1629
+ * Partially updates the caller's own schema-defined attributes. The merged
1630
+ result is validated against the user's schema before commit. Concurrent
1631
+ writes are last-write-wins.
1632
+
1633
+ * @summary Update my user information
1634
+ */
1635
+ const patchMyUser = async (patchMyUserBody, options) => {
1636
+ return customFetch(getPatchMyUserUrl(), {
1637
+ ...options,
1638
+ method: "PATCH",
1639
+ headers: {
1640
+ "Content-Type": "application/json",
1641
+ ...options?.headers
1642
+ },
1643
+ body: JSON.stringify(patchMyUserBody)
1644
+ });
1645
+ };
1646
+ const getListMyProjectsUrl = (params) => {
1647
+ const normalizedParams = new URLSearchParams();
1648
+ Object.entries(params || {}).forEach(([key, value]) => {
1649
+ if (value !== void 0) normalizedParams.append(key, value === null ? "null" : value.toString());
1650
+ });
1651
+ const stringifiedParams = normalizedParams.toString();
1652
+ return stringifiedParams.length > 0 ? `${getProxyPath()}/users/me/projects?${stringifiedParams}` : `${getProxyPath()}/users/me/projects`;
1653
+ };
1654
+ /**
1655
+ * The projects the signed-in user holds an active grant on, either directly or
1656
+ through a team they are a member of. Ordered by project id and paged with an
1657
+ opaque cursor.
1658
+
1659
+ This is not the same question as `queryProjects`, which answers with the one
1660
+ project the calling credential is bound to. Here the session is the caller
1661
+ and the grants are the answer, so the list spans projects.
1662
+
1663
+ * @summary List the projects I can act on
1664
+ */
1665
+ const listMyProjects = async (params, options) => {
1666
+ return customFetch(getListMyProjectsUrl(params), {
1667
+ ...options,
1668
+ method: "GET"
1669
+ });
1670
+ };
1540
1671
  const getCreateFlowUrl = () => {
1541
1672
  return `${getProxyPath()}/flow`;
1542
1673
  };
@@ -2222,42 +2353,6 @@ const getFlowDefinition = async (id, options) => {
2222
2353
  method: "GET"
2223
2354
  });
2224
2355
  };
2225
- const getUpdateFlowDefinitionUrl = (id) => {
2226
- return `${getProxyPath()}/flow_definitions/${id}`;
2227
- };
2228
- /**
2229
- * Update a flow definition by id. This endpoint replaces the existing flow definition.
2230
- If `flow_definition.status` is omitted, the current status is preserved
2231
-
2232
- * @summary Update a flow definition by id
2233
- */
2234
- const updateFlowDefinition = async (id, updateFlowDefinitionBody, options) => {
2235
- return customFetch(getUpdateFlowDefinitionUrl(id), {
2236
- ...options,
2237
- method: "PUT",
2238
- headers: {
2239
- "Content-Type": "application/json",
2240
- ...options?.headers
2241
- },
2242
- body: JSON.stringify(updateFlowDefinitionBody)
2243
- });
2244
- };
2245
- const getDeleteFlowDefinitionUrl = (id) => {
2246
- return `${getProxyPath()}/flow_definitions/${id}`;
2247
- };
2248
- /**
2249
- * Delete a flow definition by id.
2250
- If the flow definition is currently being used by a flow, the deletion will fail.
2251
- If the flow definition is the last active flow definition for a given purpose, the deletion will fail to prevent disruption of new flows being started for that purpose.
2252
-
2253
- * @summary Delete a flow definition by id
2254
- */
2255
- const deleteFlowDefinition = async (id, options) => {
2256
- return customFetch(getDeleteFlowDefinitionUrl(id), {
2257
- ...options,
2258
- method: "DELETE"
2259
- });
2260
- };
2261
2356
  const getCreateTeamUrl = (params) => {
2262
2357
  const normalizedParams = new URLSearchParams();
2263
2358
  Object.entries(params || {}).forEach(([key, value]) => {
@@ -2331,6 +2426,11 @@ users whose lifecycle it owns are deactivated with it.
2331
2426
  The request is idempotent. Deleting a team that is already deactivated
2332
2427
  or doesn't exist succeeds without changing anything.
2333
2428
 
2429
+ A team that still owns a project is refused with `409
2430
+ team.owns_project`: deactivating it would leave the project owned by a
2431
+ dead team and so unmanageable. No endpoint releases that ownership yet,
2432
+ so such a team cannot currently be deactivated through the API.
2433
+
2334
2434
  * @summary Delete team
2335
2435
  */
2336
2436
  const deleteTeam = async (teamId, options) => {
@@ -2410,8 +2510,7 @@ const getCreateBrandingUrl = (params) => {
2410
2510
  /**
2411
2511
  * Publishes a new immutable branding revision for the project. Branding
2412
2512
  revisions cannot be updated or deleted; every edit publishes a new
2413
- revision, and flow responses resolve the latest revision per project
2414
- (see ADR 040).
2513
+ revision, and flow responses resolve the latest revision per project.
2415
2514
 
2416
2515
  The `liquid_template` is validated lexically on save (size, encoding,
2417
2516
  banned patterns such as `<script>` tags, inline event handlers, and the
@@ -2442,9 +2541,9 @@ const getListBrandingUrl = (params) => {
2442
2541
  /**
2443
2542
  * Lists branding revisions for the project, newest first, capped at the
2444
2543
  100 most recent. The first entry is the revision flow responses
2445
- currently resolve. Deliberately unpaginated in v1 — list endpoints
2446
- gain a real query mechanism together (ADR 031); advertising pagination
2447
- parameters the server ignores would be worse than none.
2544
+ currently resolve. Deliberately unpaginated in v1 — list endpoints gain a
2545
+ real query mechanism together; advertising pagination parameters the
2546
+ server ignores would be worse than none.
2448
2547
 
2449
2548
  * @summary List branding revisions
2450
2549
  */
@@ -2467,6 +2566,142 @@ const getBrandingById = async (id, options) => {
2467
2566
  method: "GET"
2468
2567
  });
2469
2568
  };
2569
+ const getCreateIdpUrl = (params) => {
2570
+ const normalizedParams = new URLSearchParams();
2571
+ Object.entries(params || {}).forEach(([key, value]) => {
2572
+ if (value !== void 0) normalizedParams.append(key, value === null ? "null" : value.toString());
2573
+ });
2574
+ const stringifiedParams = normalizedParams.toString();
2575
+ return stringifiedParams.length > 0 ? `${getProxyPath()}/idps?${stringifiedParams}` : `${getProxyPath()}/idps`;
2576
+ };
2577
+ /**
2578
+ * Creates or revises a connection document. If the slug does not already
2579
+ exist, a new connection is created. If a connection with that slug already
2580
+ exists, a revision is created.
2581
+
2582
+ A revision gets a new `revision_id` and does not modify the connection
2583
+ `id`, so identity links that reference the connection keep resolving while
2584
+ releases and auth attempts stay pinned to the revision they captured.
2585
+
2586
+ Identity fields are fixed for the life of the connection and a revision
2587
+ that changes one is rejected: `protocol`, `subject_claim`, and the
2588
+ endpoints that name the authority (`issuer` for OIDC, `token_endpoint`
2589
+ and `userinfo_endpoint` for OAuth 2.0). Their values decide which provider
2590
+ account a stored subject belongs to, so changing one would silently
2591
+ repoint existing identities at a different provider.
2592
+
2593
+ `subject_claim` is optional for OIDC and required for OAuth 2.0. For an
2594
+ OIDC connection, `sub` is used as the default value. If a revision updates
2595
+ `subject_claim` to a different value than the one set during creation, the
2596
+ request is rejected.
2597
+
2598
+ The document is validated against the `idp-connection.json` schema before
2599
+ anything is stored.
2600
+
2601
+ * @summary Create or revise an identity provider connection
2602
+ */
2603
+ const createIdp = async (createIdpBody, params, options) => {
2604
+ return customFetch(getCreateIdpUrl(params), {
2605
+ ...options,
2606
+ method: "POST",
2607
+ headers: {
2608
+ "Content-Type": "application/json",
2609
+ ...options?.headers
2610
+ },
2611
+ body: JSON.stringify(createIdpBody)
2612
+ });
2613
+ };
2614
+ const getQueryIdpsUrl = (params) => {
2615
+ const normalizedParams = new URLSearchParams();
2616
+ Object.entries(params || {}).forEach(([key, value]) => {
2617
+ if (value !== void 0) normalizedParams.append(key, value === null ? "null" : value.toString());
2618
+ });
2619
+ const stringifiedParams = normalizedParams.toString();
2620
+ return stringifiedParams.length > 0 ? `${getProxyPath()}/idps/query?${stringifiedParams}` : `${getProxyPath()}/idps/query`;
2621
+ };
2622
+ /**
2623
+ * Returns the identity provider connections of a project, paginated with a
2624
+ cursor. One row per connection, carrying its newest revision.
2625
+
2626
+ * @summary Query identity provider connections
2627
+ */
2628
+ const queryIdps = async (queryIdpsBody, params, options) => {
2629
+ return customFetch(getQueryIdpsUrl(params), {
2630
+ ...options,
2631
+ method: "POST",
2632
+ headers: {
2633
+ "Content-Type": "application/json",
2634
+ ...options?.headers
2635
+ },
2636
+ body: JSON.stringify(queryIdpsBody)
2637
+ });
2638
+ };
2639
+ const getGetIdpByIdUrl = (id, params) => {
2640
+ const normalizedParams = new URLSearchParams();
2641
+ Object.entries(params || {}).forEach(([key, value]) => {
2642
+ if (value !== void 0) normalizedParams.append(key, value === null ? "null" : value.toString());
2643
+ });
2644
+ const stringifiedParams = normalizedParams.toString();
2645
+ return stringifiedParams.length > 0 ? `${getProxyPath()}/idps/${id}?${stringifiedParams}` : `${getProxyPath()}/idps/${id}`;
2646
+ };
2647
+ /**
2648
+ * Reads a connection by its id, at its newest revision.
2649
+ `GET /idps/{id}/revisions` lists every revision of the connection.
2650
+
2651
+ The lookup is scoped to the project in `project_id`.
2652
+
2653
+ * @summary Get an identity provider connection by id
2654
+ */
2655
+ const getIdpById = async (id, params, options) => {
2656
+ return customFetch(getGetIdpByIdUrl(id, params), {
2657
+ ...options,
2658
+ method: "GET"
2659
+ });
2660
+ };
2661
+ const getListIdpRevisionsUrl = (id, params) => {
2662
+ const normalizedParams = new URLSearchParams();
2663
+ Object.entries(params || {}).forEach(([key, value]) => {
2664
+ if (value !== void 0) normalizedParams.append(key, value === null ? "null" : value.toString());
2665
+ });
2666
+ const stringifiedParams = normalizedParams.toString();
2667
+ return stringifiedParams.length > 0 ? `${getProxyPath()}/idps/${id}/revisions?${stringifiedParams}` : `${getProxyPath()}/idps/${id}/revisions`;
2668
+ };
2669
+ /**
2670
+ * Returns every revision of one connection, newest first, paginated with a
2671
+ cursor. The order is fixed, so there is no `sorting`.
2672
+
2673
+ The lookup is scoped to the project in `project_id`.
2674
+
2675
+ * @summary List the revisions of an identity provider connection
2676
+ */
2677
+ const listIdpRevisions = async (id, params, options) => {
2678
+ return customFetch(getListIdpRevisionsUrl(id, params), {
2679
+ ...options,
2680
+ method: "GET"
2681
+ });
2682
+ };
2683
+ const getGetIdpRevisionByIdUrl = (revisionId, params) => {
2684
+ const normalizedParams = new URLSearchParams();
2685
+ Object.entries(params || {}).forEach(([key, value]) => {
2686
+ if (value !== void 0) normalizedParams.append(key, value === null ? "null" : value.toString());
2687
+ });
2688
+ const stringifiedParams = normalizedParams.toString();
2689
+ return stringifiedParams.length > 0 ? `${getProxyPath()}/idps/revisions/${revisionId}?${stringifiedParams}` : `${getProxyPath()}/idps/revisions/${revisionId}`;
2690
+ };
2691
+ /**
2692
+ * Reads one revision of a connection by its `revision_id`, the value an auth
2693
+ attempt or a release pins.
2694
+
2695
+ The lookup is scoped to the project in `project_id`.
2696
+
2697
+ * @summary Get one revision of an identity provider connection
2698
+ */
2699
+ const getIdpRevisionById = async (revisionId, params, options) => {
2700
+ return customFetch(getGetIdpRevisionByIdUrl(revisionId, params), {
2701
+ ...options,
2702
+ method: "GET"
2703
+ });
2704
+ };
2470
2705
  const getCreateReleaseUrl = (params) => {
2471
2706
  const normalizedParams = new URLSearchParams();
2472
2707
  Object.entries(params || {}).forEach(([key, value]) => {
@@ -2566,10 +2801,23 @@ const getCreateGrantUrl = (params) => {
2566
2801
  /**
2567
2802
  * Bind a user or team to `project.viewer`, `project.editor`, or
2568
2803
  `project.admin` on the project identified by the `project-id` header.
2569
- IDs are `asgn_<opaque>`. Owning-team (`project.team`) grants are not
2570
- created here — claim owns that path. An unrevoked grant with the same
2571
- principal and relation occupies the unique key even after `expires_at`;
2572
- DELETE it before re-creating.
2804
+ Name the principal with `user` (`user_id` or `identifier`) or `team`
2805
+ (`team_id` or `name`). IDs are `asgn_<opaque>`. Owning-team
2806
+ (`project.team`) grants are not created here — claim owns that path. An
2807
+ unrevoked grant with the same principal and relation occupies the unique
2808
+ key even after `expires_at`; DELETE it before re-creating.
2809
+ Create does not accept `expand`. The `user_id` and team locators
2810
+ return 201, whose `user` / `team` carry only `user_id` / `team_id`,
2811
+ and still 404 / 409 when the principal is missing or the tuple
2812
+ already exists. Creating by `user.identifier` answers 202 with no
2813
+ body on every outcome. Granting the session caller's own user is
2814
+ `grant.invalid` on either user locator. Read the grant back for
2815
+ identifier and display.
2816
+
2817
+ Accepts either a project secret (`oauth2`) or a user-bound Console
2818
+ session cookie (`nextgenSession`). Session callers are authorized as
2819
+ the human against the target project (home may differ). CSRF/Origin
2820
+ for cookie mutations is a follow-up (#1140).
2573
2821
 
2574
2822
  * @summary Create grant
2575
2823
  */
@@ -2599,10 +2847,16 @@ viewer, editor, or admin), including expired grants so a client can
2599
2847
  DELETE before re-granting. Project-secret setup (`sk_proj`) and
2600
2848
  owning-team (`relation=team`) rows are not returned. Grants are not in
2601
2849
  `resource_scope_index`; project scope is required on the query (same as
2602
- get). Requires `project.read`. `expand: ["principal"]` additionally
2603
- requires `user.read` and `team.read` (documented on the expand enum;
2604
- those scopes cannot be ANDed onto this security block because they are
2605
- body-conditional).
2850
+ get). Requires `project.read`. `expand: ["principal"]` adds envelope
2851
+ fields on `user` / `team` and additionally requires `user.read` and
2852
+ `team.read` for project secrets (documented on the expand enum; those
2853
+ scopes cannot be ANDed onto this security block because they are
2854
+ body-conditional). A user-bound Console session that already passed
2855
+ the project Check may expand without those scopes.
2856
+
2857
+ Accepts either a project secret (`oauth2`) or a user-bound Console
2858
+ session cookie (`nextgenSession`). CSRF/Origin for cookie mutations
2859
+ is a follow-up (#1140).
2606
2860
 
2607
2861
  * @summary Query grants
2608
2862
  */
@@ -2634,6 +2888,14 @@ still ignores expired grants. Grants are not registered in
2634
2888
  `resource_scope_index`; project scope is required on the query (same as
2635
2889
  events). Misses, revoked rows, project-secret setup (`sk_proj`),
2636
2890
  owning-team (`relation=team`) rows, and cross-project ids return 404.
2891
+ `expand=principal` adds envelope fields on `user` or `team` and requires
2892
+ `user.read` and `team.read` in addition to `project.read` for project
2893
+ secrets. A user-bound Console session that already passed the project
2894
+ Check may expand without those scopes.
2895
+
2896
+ Accepts either a project secret (`oauth2`) or a user-bound Console
2897
+ session cookie (`nextgenSession`). CSRF/Origin for cookie mutations
2898
+ is a follow-up (#1140).
2637
2899
 
2638
2900
  * @summary Get grant
2639
2901
  */
@@ -2657,6 +2919,10 @@ Already-revoked, missing, project-secret setup, and owning-team grants
2657
2919
  return 404. The row is not un-revoked. Expired grants can still be
2658
2920
  revoked so the unique binding can be reused.
2659
2921
 
2922
+ Accepts either a project secret (`oauth2`) or a user-bound Console
2923
+ session cookie (`nextgenSession`). CSRF/Origin for cookie mutations
2924
+ is a follow-up (#1140).
2925
+
2660
2926
  * @summary Revoke grant
2661
2927
  */
2662
2928
  const deleteGrant = async (id, params, options) => {
@@ -2723,6 +2989,122 @@ const getEvent = async (id, params, options) => {
2723
2989
  method: "GET"
2724
2990
  });
2725
2991
  };
2992
+ const getGetVariablesUrl = (params) => {
2993
+ const normalizedParams = new URLSearchParams();
2994
+ Object.entries(params || {}).forEach(([key, value]) => {
2995
+ if (value !== void 0) normalizedParams.append(key, value === null ? "null" : value.toString());
2996
+ });
2997
+ const stringifiedParams = normalizedParams.toString();
2998
+ return stringifiedParams.length > 0 ? `${getProxyPath()}/variables?${stringifiedParams}` : `${getProxyPath()}/variables`;
2999
+ };
3000
+ /**
3001
+ * Returns the variables entered at the owner this request addresses, keyed by
3002
+ name — one environment of the project with `environment_name`, the project
3003
+ level itself without it.
3004
+
3005
+ Owners are separate, not a ladder: an environment does not inherit the
3006
+ project's variables and the project does not see its environments'. Reading
3007
+ everything a project holds therefore means reading each owner in turn.
3008
+
3009
+ Secret values are not returned. A secret appears as `{"secret": true}`,
3010
+ which says a value is held without disclosing it.
3011
+
3012
+ * @summary Get variables
3013
+ */
3014
+ const getVariables = async (params, options) => {
3015
+ return customFetch(getGetVariablesUrl(params), {
3016
+ ...options,
3017
+ method: "GET"
3018
+ });
3019
+ };
3020
+ const getUpdateVariablesUrl = (params) => {
3021
+ const normalizedParams = new URLSearchParams();
3022
+ Object.entries(params || {}).forEach(([key, value]) => {
3023
+ if (value !== void 0) normalizedParams.append(key, value === null ? "null" : value.toString());
3024
+ });
3025
+ const stringifiedParams = normalizedParams.toString();
3026
+ return stringifiedParams.length > 0 ? `${getProxyPath()}/variables?${stringifiedParams}` : `${getProxyPath()}/variables`;
3027
+ };
3028
+ /**
3029
+ * Enters, replaces and removes variables at the owner this request
3030
+ addresses.
3031
+
3032
+ Every name in the body is applied at exactly that owner — the project, or
3033
+ the environment named by `environment_name` — and reaches no other. Names
3034
+ not in the body are untouched.
3035
+
3036
+ A bare scalar enters a non-secret value. `{"value": …, "secret": true}`
3037
+ stores the value encrypted under the project's active `secret` key, after
3038
+ which it can be referenced but not read back. `null` removes the name from
3039
+ this owner (RFC 7386), which is how several variables are removed in one
3040
+ request; removing a name this owner does not hold is a no-op rather than an
3041
+ error.
3042
+
3043
+ The body is applied whole or not at all, so a rejected request leaves the
3044
+ owner exactly as it was. Writing the same name and owner twice replaces the
3045
+ value rather than duplicating it, which makes a retry safe.
3046
+
3047
+ * @summary Update variables
3048
+ */
3049
+ const updateVariables = async (updateVariablesBody, params, options) => {
3050
+ return customFetch(getUpdateVariablesUrl(params), {
3051
+ ...options,
3052
+ method: "PATCH",
3053
+ headers: {
3054
+ "Content-Type": "application/json",
3055
+ ...options?.headers
3056
+ },
3057
+ body: JSON.stringify(updateVariablesBody)
3058
+ });
3059
+ };
3060
+ const getGetVariableUrl = (variableName, params) => {
3061
+ const normalizedParams = new URLSearchParams();
3062
+ Object.entries(params || {}).forEach(([key, value]) => {
3063
+ if (value !== void 0) normalizedParams.append(key, value === null ? "null" : value.toString());
3064
+ });
3065
+ const stringifiedParams = normalizedParams.toString();
3066
+ return stringifiedParams.length > 0 ? `${getProxyPath()}/variables/${variableName}?${stringifiedParams}` : `${getProxyPath()}/variables/${variableName}`;
3067
+ };
3068
+ /**
3069
+ * Reads one variable by name from the owner this request addresses — one
3070
+ environment of the project with `environment_name`, the project level
3071
+ itself without it.
3072
+
3073
+ A name that owner has not entered answers `var.not_found`, even when
3074
+ another owner of the same project holds it: nothing is inherited. A secret
3075
+ is found but not disclosed: the response is `{"secret": true}`.
3076
+
3077
+ * @summary Get a variable
3078
+ */
3079
+ const getVariable = async (variableName, params, options) => {
3080
+ return customFetch(getGetVariableUrl(variableName, params), {
3081
+ ...options,
3082
+ method: "GET"
3083
+ });
3084
+ };
3085
+ const getDeleteVariableUrl = (variableName, params) => {
3086
+ const normalizedParams = new URLSearchParams();
3087
+ Object.entries(params || {}).forEach(([key, value]) => {
3088
+ if (value !== void 0) normalizedParams.append(key, value === null ? "null" : value.toString());
3089
+ });
3090
+ const stringifiedParams = normalizedParams.toString();
3091
+ return stringifiedParams.length > 0 ? `${getProxyPath()}/variables/${variableName}?${stringifiedParams}` : `${getProxyPath()}/variables/${variableName}`;
3092
+ };
3093
+ /**
3094
+ * Removes the variable this owner entered under this name.
3095
+
3096
+ A variable is deletable only by the owner that entered it. Deleting a name
3097
+ another owner of the same project holds answers `var.not_found` and leaves
3098
+ that owner's value standing.
3099
+
3100
+ * @summary Delete a variable
3101
+ */
3102
+ const deleteVariable = async (variableName, params, options) => {
3103
+ return customFetch(getDeleteVariableUrl(variableName, params), {
3104
+ ...options,
3105
+ method: "DELETE"
3106
+ });
3107
+ };
2726
3108
  //#endregion
2727
3109
  //#region ../api/dist/runtime/api-factory.mjs
2728
3110
  /**
@@ -3038,9 +3420,10 @@ function createSanitiser() {
3038
3420
  * the `Branding` JSON into a CSSStyleSheet of `:host { --zl-* }` declarations
3039
3421
  * and applies it via `shadowRoot.adoptedStyleSheets`.
3040
3422
  *
3041
- * Dark-mode overrides are emitted as `:host([data-theme="dark"]) { ... }`
3042
- * (matching `docs/design/branding/tokens.md`). Resolution between
3043
- * `light | dark | auto` happens in `<zitadel-login>`.
3423
+ * Light and dark are independent surfaces: each side's palette is emitted under
3424
+ * its own `data-theme` selector only, so a key the revision left unset on one
3425
+ * side takes the maintained default for that side rather than the other side's
3426
+ * colour. Resolution between `light | dark | auto` happens in `<zitadel-login>`.
3044
3427
  *
3045
3428
  * Base layer: the design-tokens package ships the full `--zl-*` set as both a
3046
3429
  * `.css` file (for host pages) and a `tokensCss` string (for shadow roots and
@@ -3048,13 +3431,32 @@ function createSanitiser() {
3048
3431
  * paint correctly even when the host page didn't `@import` tokens.css — the
3049
3432
  * orchestrator is meant to drop into any page.
3050
3433
  */
3051
- const RADIUS_MAP = {
3052
- none: "0",
3053
- sm: "0.25rem",
3054
- md: "0.5rem",
3055
- lg: "0.75rem",
3056
- full: "9999px"
3057
- };
3434
+ const RADIUS_STEPS = [
3435
+ "xs",
3436
+ "sm",
3437
+ "md",
3438
+ "lg",
3439
+ "xl"
3440
+ ];
3441
+ const RADIUS_RATIOS = (() => {
3442
+ const base = remToNumber(tokens.radius.md);
3443
+ const ratios = {};
3444
+ for (const step of RADIUS_STEPS) ratios[step] = remToNumber(tokens.radius[step]) / base;
3445
+ return ratios;
3446
+ })();
3447
+ const RADIUS_PRESETS = {
3448
+ none: 0,
3449
+ sm: .25,
3450
+ md: .5,
3451
+ lg: .75
3452
+ };
3453
+ const TEXT_STEPS = [
3454
+ "xs",
3455
+ "sm",
3456
+ "base",
3457
+ "lg",
3458
+ "xl"
3459
+ ];
3058
3460
  const DENSITY_MAP = {
3059
3461
  compact: {
3060
3462
  "--zl-spacing-4": "0.75rem",
@@ -3106,34 +3508,35 @@ const PALETTE_MAP = {
3106
3508
  * the moment before a theme resolves.
3107
3509
  */
3108
3510
  const BRANDING_SELECTOR = ":host, :host([data-theme=\"light\"]), :host([data-theme=\"dark\"])";
3109
- /** Dark-only overrides. Emitted after {@link BRANDING_SELECTOR}, so it wins on order. */
3511
+ /** Per-side palettes. Emitted after {@link BRANDING_SELECTOR}, so they win on order. */
3512
+ const LIGHT_SELECTOR = ":host([data-theme=\"light\"])";
3110
3513
  const DARK_SELECTOR = ":host([data-theme=\"dark\"])";
3111
3514
  /**
3112
3515
  * Build a CSS string of `:host { --zl-* }` declarations from a Branding
3113
- * payload. Light values land on `:host`; dark overrides land on
3114
- * `:host([data-theme="dark"])`.
3516
+ * payload. Shape and typography are shared across the sides and land on the
3517
+ * shared selector; each side's palette lands only under its own `data-theme`
3518
+ * selector, and nowhere else.
3519
+ *
3520
+ * No copy of the resolved side goes on the shared selector. That copy would
3521
+ * reach `[data-theme="light"]` as well, so a key set on the dark side and not
3522
+ * the light one would paint on both, with nothing in the light block to
3523
+ * override it. Callers stamp `data-theme` on the host — `applySurfaceTheme`
3524
+ * does it in the same update that adopts this sheet — so the per-side block is
3525
+ * already matching by the time anything paints.
3115
3526
  */
3116
- function buildBrandingStylesheet(branding, options = {}) {
3117
- const lightDecls = collectDeclarations(branding);
3118
- const darkPalette = branding?.theme?.dark?.palette;
3119
- const darkDecls = darkPalette ? mapPalette(darkPalette) : {};
3527
+ function buildBrandingStylesheet(branding) {
3528
+ const shared = collectDeclarations(branding);
3529
+ const light = mapPalette(branding?.theme?.light?.palette);
3530
+ const dark = mapPalette(branding?.theme?.dark?.palette);
3120
3531
  const blocks = [];
3121
- if (Object.keys(lightDecls).length > 0) blocks.push(formatBlock(BRANDING_SELECTOR, lightDecls));
3122
- if (Object.keys(darkDecls).length > 0) blocks.push(formatBlock(DARK_SELECTOR, darkDecls));
3123
- if (options.resolvedTheme === "dark" && darkPalette) {
3124
- const merged = {
3125
- ...lightDecls,
3126
- ...darkDecls
3127
- };
3128
- blocks.length = 0;
3129
- blocks.push(formatBlock(BRANDING_SELECTOR, merged));
3130
- }
3532
+ if (Object.keys(shared).length > 0) blocks.push(formatBlock(BRANDING_SELECTOR, shared));
3533
+ if (Object.keys(light).length > 0) blocks.push(formatBlock(LIGHT_SELECTOR, light));
3534
+ if (Object.keys(dark).length > 0) blocks.push(formatBlock(DARK_SELECTOR, dark));
3131
3535
  return blocks.join("\n");
3132
3536
  }
3133
3537
  function collectDeclarations(branding) {
3134
3538
  if (!branding) return {};
3135
3539
  const decls = {};
3136
- Object.assign(decls, mapPalette(branding.palette));
3137
3540
  Object.assign(decls, mapTypography(branding.typography));
3138
3541
  Object.assign(decls, mapShape(branding.shape));
3139
3542
  return decls;
@@ -3154,24 +3557,52 @@ function mapTypography(typography) {
3154
3557
  out["--zl-font-family-sans"] = typography.font_family;
3155
3558
  out["--zl-font-family-heading"] = typography.font_family;
3156
3559
  }
3157
- if (typography.font_family_heading) out["--zl-font-family-heading"] = typography.font_family_heading;
3158
- if (typography.font_family_mono) out["--zl-font-family-mono"] = typography.font_family_mono;
3159
3560
  const scale = clamp(typography.scale ?? 1, .75, 1.25);
3160
- if (scale !== 1) out["--zl-font-scale"] = `${scale}`;
3561
+ if (scale !== 1) for (const step of TEXT_STEPS) {
3562
+ out[`--zl-text-${step}-size`] = scaleRem(tokens.text[step].size, scale);
3563
+ out[`--zl-text-${step}-leading`] = scaleRem(tokens.text[step].leading, scale);
3564
+ }
3161
3565
  return out;
3162
3566
  }
3163
3567
  function mapShape(shape) {
3164
3568
  if (!shape) return {};
3165
3569
  const out = {};
3166
- if (shape.radius && RADIUS_MAP[shape.radius]) {
3167
- const radius = RADIUS_MAP[shape.radius];
3168
- out["--zl-radius-md"] = radius;
3169
- out["--zl-radius-lg"] = radius === "0" ? "0" : `calc(${radius} * 1.25)`;
3170
- out["--zl-radius-xl"] = radius === "0" ? "0" : `calc(${radius} * 1.75)`;
3171
- }
3570
+ Object.assign(out, mapRadius(shape.radius));
3172
3571
  if (shape.density) Object.assign(out, DENSITY_MAP[shape.density]);
3572
+ const logoScale = shape.logo_scale;
3573
+ if (typeof logoScale === "number" && Number.isFinite(logoScale)) out["--zl-logo-scale"] = `${clamp(logoScale, .5, 2)}`;
3574
+ return out;
3575
+ }
3576
+ /**
3577
+ * A brand picks one corner value and the whole ramp follows it in proportion,
3578
+ * which is what keeps the card rounder than the controls inside it. Both forms
3579
+ * the revision accepts land here: a preset name, or an integer number of pixels
3580
+ * for the brand that has a specific value.
3581
+ */
3582
+ function mapRadius(radius) {
3583
+ if (radius == null) return {};
3584
+ const out = {};
3585
+ if (radius === "full") {
3586
+ for (const step of RADIUS_STEPS) out[`--zl-radius-${step}`] = tokens.radius.full;
3587
+ return out;
3588
+ }
3589
+ const [base, unit] = typeof radius === "number" ? [radius, "px"] : radius in RADIUS_PRESETS ? [RADIUS_PRESETS[radius], "rem"] : [NaN, ""];
3590
+ if (Number.isNaN(base)) return {};
3591
+ for (const step of RADIUS_STEPS) {
3592
+ const value = base * RADIUS_RATIOS[step];
3593
+ out[`--zl-radius-${step}`] = value === 0 ? "0" : `${round(value)}${unit}`;
3594
+ }
3173
3595
  return out;
3174
3596
  }
3597
+ function scaleRem(value, scale) {
3598
+ return `${round(remToNumber(value) * scale)}rem`;
3599
+ }
3600
+ function remToNumber(value) {
3601
+ return Number.parseFloat(value);
3602
+ }
3603
+ function round(value) {
3604
+ return Math.round(value * 1e4) / 1e4;
3605
+ }
3175
3606
  function formatBlock(selector, decls) {
3176
3607
  return `${selector} {\n${Object.entries(decls).map(([prop, value]) => ` ${prop}: ${value};`).join("\n")}\n}`;
3177
3608
  }
@@ -3211,8 +3642,8 @@ function applyBaseTokens(shadowRoot) {
3211
3642
  * Callers should run `applyBaseTokens(shadowRoot)` first (once per shadow
3212
3643
  * root) so the base values exist before branding patches them.
3213
3644
  */
3214
- function applyBrandingTokens(shadowRoot, branding, resolvedTheme) {
3215
- const css = buildBrandingStylesheet(branding, { resolvedTheme });
3645
+ function applyBrandingTokens(shadowRoot, branding) {
3646
+ const css = buildBrandingStylesheet(branding);
3216
3647
  if (typeof CSSStyleSheet === "undefined") return;
3217
3648
  const sheet = new CSSStyleSheet();
3218
3649
  sheet.replaceSync(css);
@@ -3227,9 +3658,11 @@ function applyBrandingTokens(shadowRoot, branding, resolvedTheme) {
3227
3658
  * and a variant-derived fallback on top of the same branding input.
3228
3659
  *
3229
3660
  * Defaults to dark: the design system's primary surface, and the mode a
3230
- * hosted login page renders when a tenant states no preference.
3661
+ * hosted login page renders when a revision states no preference.
3231
3662
  */
3232
3663
  function resolveTheme(branding) {
3664
+ const [only, second] = publishedSides(branding);
3665
+ if (only && !second) return only;
3233
3666
  const mode = branding?.theme?.mode ?? "dark";
3234
3667
  if (mode === "light") return "light";
3235
3668
  if (mode === "dark") return "dark";
@@ -3247,7 +3680,7 @@ function resolveTheme(branding) {
3247
3680
  * (Arimo, named first in `--zl-font-family-sans`). The component ships this
3248
3681
  * so the auth screens render in the brand font out of the box, even when the
3249
3682
  * server returns no branding. This is the "default users can override" layer.
3250
- * - The **tenant override** (`applyFontUrl`) — `branding.font_url`, resolved
3683
+ * - The **tenant override** (`applyFontUrl`) — `branding.typography.font_url`, resolved
3251
3684
  * server-side from the app → team → project branding hierarchy.
3252
3685
  *
3253
3686
  * Both must live at document level, not inside the shadow root: browsers ignore
@@ -3276,7 +3709,7 @@ const DEFAULT_LINK_ID = "zl-default-font-link";
3276
3709
  * Design-system default brand font. Arimo is the Figma body face and leads the
3277
3710
  * `--zl-font-family-sans` stack; loading it here makes the unbranded auth UI
3278
3711
  * paint in the brand font instead of the system fallback. Tenants override it
3279
- * with `branding.font_url`. Exported so a privacy- or offline-sensitive
3712
+ * with `branding.typography.font_url`. Exported so a privacy- or offline-sensitive
3280
3713
  * deployment can self-host the same face and swap this single URL.
3281
3714
  */
3282
3715
  const DEFAULT_BRAND_FONT_HREF = "https://fonts.googleapis.com/css2?family=Arimo:ital,wght@0,400;0,500;0,600;0,700;1,400&display=swap";
@@ -3296,7 +3729,7 @@ const DEFAULT_BRAND_FONT_HREF = "https://fonts.googleapis.com/css2?family=Arimo:
3296
3729
  /** Per-document, per-link-id map of each owner's desired href. */
3297
3730
  const registries = /* @__PURE__ */ new WeakMap();
3298
3731
  /**
3299
- * Inject the tenant's `branding.font_url` stylesheet. Pass `null`/`undefined`
3732
+ * Inject the tenant's `branding.typography.font_url` stylesheet. Pass `null`/`undefined`
3300
3733
  * to withdraw this surface's registration (e.g. when a flow resolves with no
3301
3734
  * tenant font); the link disappears once no surface registers a URL.
3302
3735
  */
@@ -3306,7 +3739,7 @@ function applyFontUrl(shadowRoot, fontUrl) {
3306
3739
  /**
3307
3740
  * Inject the design-system default font stylesheet. Defaults to
3308
3741
  * {@link DEFAULT_BRAND_FONT_HREF}; pass `null` to withdraw this surface's
3309
- * registration (the orchestrator does this when a tenant `font_url` takes
3742
+ * registration (the orchestrator does this when a tenant font stylesheet takes
3310
3743
  * over, to avoid a redundant request).
3311
3744
  */
3312
3745
  function applyDefaultFont(shadowRoot, href = DEFAULT_BRAND_FONT_HREF) {
@@ -3373,7 +3806,10 @@ var ThemeController = class {
3373
3806
  * element nor the branding payload names a mode. Precedence, strongest
3374
3807
  * first: element `theme` property → `branding.theme.mode` → fallback. The
3375
3808
  * element wins because the page embedding the widget knows its own surface
3376
- * better than the tenant's stored branding does.
3809
+ * better than the tenant's stored branding does — but it selects among the
3810
+ * sides the revision published, and cannot reach one it did not. A revision
3811
+ * with a single published side therefore ignores this property: an embedder
3812
+ * pinning `theme="light"` against a dark-only revision gets dark.
3377
3813
  */
3378
3814
  setModePreference(explicit, fallback) {
3379
3815
  if (this.explicitMode === explicit && this.fallbackMode === fallback) return;
@@ -3388,6 +3824,12 @@ var ThemeController = class {
3388
3824
  this.detach();
3389
3825
  }
3390
3826
  refresh() {
3827
+ const [only, second] = publishedSides(this.branding);
3828
+ if (only && !second) {
3829
+ this.detach();
3830
+ this.update(only);
3831
+ return;
3832
+ }
3391
3833
  const mode = this.explicitMode ?? this.branding?.theme?.mode ?? this.fallbackMode;
3392
3834
  if (mode === "auto") {
3393
3835
  this.attach();
@@ -3466,9 +3908,10 @@ var ZitadelSurface = class extends LitElement {
3466
3908
  const root = this.shadowRoot;
3467
3909
  if (root) {
3468
3910
  applyBaseTokens(root);
3469
- applyBrandingTokens(root, branding, this.themeController.theme);
3470
- const tenantFontUrl = branding?.font_url ?? null;
3471
- applyDefaultFont(root, this.variant === "page" && !tenantFontUrl ? void 0 : null);
3911
+ applyBrandingTokens(root, branding);
3912
+ const page = this.variant === "page";
3913
+ const tenantFontUrl = page ? branding?.typography?.font_url ?? null : null;
3914
+ applyDefaultFont(root, page && !tenantFontUrl ? void 0 : null);
3472
3915
  applyFontUrl(root, tenantFontUrl);
3473
3916
  }
3474
3917
  this.dataset.theme = this.themeController.theme;
@@ -3504,7 +3947,7 @@ const TEMPLATE_NAMES = {
3504
3947
  };
3505
3948
  //#endregion
3506
3949
  //#region src/orchestrator/templates/layout-chrome.css?inline
3507
- var layout_chrome_default = ":host {\n color: var(--zl-foreground);\n font-family: var(--zl-font-family-sans);\n background: none;\n display: block;\n}\n\n:host([variant=\"page\"]) {\n background: var(--zl-background);\n min-height: 100%;\n}\n\nimg[data-zl-asset-broken] {\n display: none;\n}\n\n.zl-mount {\n min-height: var(--zl-page-min-height, auto);\n background: inherit;\n color: inherit;\n flex-direction: column;\n display: flex;\n container-type: inline-size;\n}\n\n:host([variant=\"page\"]) .zl-mount {\n min-height: var(--zl-page-min-height, 100vh);\n}\n\n.zl-mount > zl-page-shell, .zl-mount > [data-zl-template-root] {\n flex: auto;\n}\n\n.zl-attribution {\n background: none;\n justify-content: center;\n align-items: center;\n display: flex;\n}\n\n.zl-attribution[hidden] {\n display: none;\n}\n\n.zl-trustmark {\n gap: var(--zl-spacing-2-5);\n flex-wrap: wrap;\n}\n\n.zl-trustmark__mark {\n align-items: center;\n gap: var(--zl-spacing-2);\n font-family: var(--zl-font-family-sans);\n font-size: var(--zl-text-sm-size);\n line-height: var(--zl-text-sm-leading);\n color: var(--zl-foreground);\n text-decoration: none;\n display: inline-flex;\n}\n\n.zl-trustmark__mark:hover {\n text-underline-offset: 4px;\n text-decoration: underline;\n}\n\n.zl-trustmark__mark:focus-visible {\n outline: var(--zl-focus-width) solid var(--zl-ring);\n outline-offset: var(--zl-focus-offset);\n border-radius: var(--zl-radius-sm);\n}\n\n.zl-trustmark__logotype {\n flex-shrink: 0;\n width: 65px;\n height: 16px;\n line-height: 0;\n display: block;\n}\n\n.zl-trustmark__logotype-svg {\n width: 100%;\n height: 100%;\n display: block;\n}\n\n.zl-card-logo {\n width: auto;\n max-width: min(12rem, 60%);\n max-height: var(--zl-card-logo-max-height, 2.5rem);\n object-fit: contain;\n}\n\n.zl-card-title {\n font-family: var(--zl-font-family-heading);\n font-size: var(--zl-text-xl-size);\n font-weight: var(--zl-font-weight-medium);\n color: var(--zl-card-foreground);\n text-align: left;\n margin: 0;\n line-height: 1;\n}\n\n.zl-card-subtitle {\n font-family: var(--zl-font-family-sans);\n font-size: var(--zl-text-sm-size);\n line-height: var(--zl-text-sm-leading);\n font-weight: var(--zl-font-weight-normal);\n color: var(--zl-muted-foreground);\n margin: 0;\n}\n\n.zl-field-group {\n gap: var(--zl-spacing-7);\n flex-direction: column;\n display: flex;\n}\n\n.zl-field-group:not(:has(*)) {\n display: none;\n}\n\n.zl-card-actions {\n gap: var(--zl-spacing-3);\n flex-direction: column;\n display: flex;\n}\n\n[data-suppress-header] .zl-card-title, [data-suppress-header] .zl-card-subtitle {\n clip-path: inset(50%);\n white-space: nowrap;\n border: 0;\n width: 1px;\n height: 1px;\n margin: -1px;\n padding: 0;\n position: absolute;\n overflow: hidden;\n}\n\n.zl-card-nav {\n font-family: var(--zl-font-family-sans);\n font-size: var(--zl-text-sm-size);\n line-height: var(--zl-text-sm-leading);\n font-weight: var(--zl-font-weight-normal);\n color: var(--zl-muted-foreground);\n text-align: center;\n margin: 0;\n}\n\n.zl-card-nav__link {\n font: inherit;\n line-height: inherit;\n color: var(--zl-link);\n text-underline-offset: 4px;\n cursor: pointer;\n background: none;\n border: none;\n margin: 0;\n padding: 0;\n text-decoration: underline;\n display: inline;\n}\n\n.zl-card-nav__link:hover {\n color: var(--zl-foreground);\n}\n\n.zl-card-nav__link:focus-visible {\n outline: var(--zl-focus-width) solid var(--zl-ring);\n outline-offset: var(--zl-focus-offset);\n}\n\n.zl-card-forgot {\n text-align: right;\n margin: 0;\n}\n\n.zl-card-forgot__link {\n font-family: var(--zl-font-family-sans);\n font-size: var(--zl-text-sm-size);\n line-height: var(--zl-text-sm-leading);\n font-weight: var(--zl-font-weight-normal);\n color: var(--zl-link);\n text-underline-offset: 4px;\n cursor: pointer;\n background: none;\n border: none;\n margin: 0;\n padding: 0;\n text-decoration: none;\n display: inline;\n}\n\n.zl-card-forgot__link:hover {\n text-decoration: underline;\n}\n\n.zl-card-forgot__link:focus-visible {\n outline: var(--zl-focus-width) solid var(--zl-ring);\n outline-offset: var(--zl-focus-offset);\n}\n\n.zl-split {\n grid-template-columns: var(--zl-split-columns, minmax(0, 1fr) minmax(0, 1fr));\n align-items: var(--zl-split-align, center);\n gap: var(--zl-spacing-8);\n width: 100%;\n min-height: 100%;\n display: grid;\n}\n\n.zl-split__brand {\n justify-content: center;\n align-items: center;\n gap: var(--zl-spacing-6);\n padding: var(--zl-spacing-8) var(--zl-spacing-4);\n flex-direction: column;\n display: flex;\n}\n\n.zl-split__logo {\n max-width: min(16rem, 60%);\n max-height: var(--zl-split-logo-max-height, 16rem);\n}\n\n.zl-split__logo:has( + .zl-split__hero) {\n max-height: var(--zl-split-logo-max-height, 8rem);\n}\n\n.zl-split__logo:has( + .zl-split__hero) + .zl-split__hero {\n max-height: min(var(--zl-split-hero-max-height, 22.5rem),\n max(6rem,\n calc(100vh - 104px - var(--zl-spacing-8) - var(--zl-spacing-8) -\n 2.5rem - var(--zl-spacing-6) - var(--zl-spacing-6) -\n var(--zl-split-logo-max-height, 8rem))));\n}\n\n.zl-split__hero {\n width: 100%;\n max-height: var(--zl-split-hero-max-height, min(22.5rem, 60vh));\n border-radius: var(--zl-radius-lg);\n object-fit: cover;\n}\n\n.zl-split__placeholder {\n aspect-ratio: 4 / 5;\n border-radius: var(--zl-radius-lg);\n background: radial-gradient(120% 120% at 18% 12%,\n var(--zl-gradient-lavender-start) 0%,\n transparent 60%),\n radial-gradient(130% 130% at 85% 92%,\n var(--zl-gradient-rose-start) 0%,\n transparent 60%),\n var(--zl-card);\n width: min(18rem, 85%);\n}\n\n.zl-split__form {\n justify-content: center;\n align-items: center;\n gap: var(--zl-spacing-6);\n flex-direction: column;\n padding: 16px;\n display: flex;\n}\n\n.zl-split__compact {\n max-width: min(12rem, 60%);\n display: none;\n}\n\nimg.zl-split__compact {\n max-height: 2.5rem;\n}\n\nimg.zl-split__compact--hero {\n border-radius: var(--zl-radius-lg);\n object-fit: cover;\n width: 100%;\n max-width: 100%;\n max-height: 6rem;\n}\n\n.zl-split--right .zl-split__brand {\n order: 2;\n}\n\n.zl-split--right .zl-split__form {\n order: 1;\n}\n\nzl-page-shell:has(.zl-split) [slot=\"footer\"].zl-attribution {\n grid-template-columns: var(--zl-split-columns, minmax(0, 1fr) minmax(0, 1fr));\n gap: var(--zl-spacing-8);\n width: 100%;\n display: grid;\n}\n\nzl-page-shell:has(.zl-split) [slot=\"footer\"].zl-attribution > * {\n grid-column: 2;\n justify-self: center;\n}\n\nzl-page-shell:has(.zl-split--right) [slot=\"footer\"].zl-attribution > * {\n grid-column: 1;\n}\n\nzl-page-shell:has(.zl-split) [slot=\"footer\"].zl-attribution ::slotted(*) {\n grid-column: 2;\n justify-self: center;\n}\n\nzl-page-shell:has(.zl-split--right) [slot=\"footer\"].zl-attribution ::slotted(*) {\n grid-column: 1;\n}\n\n@container (width <= 48rem) {\n .zl-split {\n grid-template-columns: minmax(0, 1fr);\n }\n\n .zl-split__brand {\n display: var(--zl-split-brand-mobile, none);\n }\n\n .zl-split__compact {\n display: block;\n }\n\n zl-page-shell:has(.zl-split) [slot=\"footer\"].zl-attribution {\n grid-template-columns: minmax(0, 1fr);\n }\n\n zl-page-shell:has(.zl-split) .zl-attribution > *, zl-page-shell:has(.zl-split--right) [slot=\"footer\"].zl-attribution > * {\n grid-column: 1;\n }\n}\n\n.zl-hero {\n text-align: left;\n width: 100%;\n max-width: 34rem;\n color: var(--zl-foreground);\n font-family: var(--zl-font-family-sans);\n flex-direction: column;\n display: flex;\n}\n\n.zl-hero__nav {\n margin-bottom: var(--zl-spacing-8);\n justify-content: space-between;\n align-items: center;\n display: flex;\n}\n\n.zl-hero__brand {\n align-items: center;\n gap: var(--zl-spacing-2);\n letter-spacing: -.01em;\n font-size: 1.125rem;\n font-weight: 700;\n display: inline-flex;\n}\n\n.zl-hero__brand-logo {\n max-height: 1.75rem;\n}\n\n.zl-hero__brand-mark {\n border-radius: var(--zl-radius-md);\n background: linear-gradient(135deg,\n var(--zl-gradient-lavender-start),\n var(--zl-gradient-rose-start));\n width: 1.75rem;\n height: 1.75rem;\n display: inline-flex;\n}\n\n.zl-hero__compact-brand {\n overflow-wrap: anywhere;\n letter-spacing: -.01em;\n max-width: 100%;\n color: var(--zl-foreground);\n margin: 0;\n font-size: 1.125rem;\n font-weight: 700;\n}\n\n.zl-hero__nav-links {\n gap: var(--zl-spacing-6);\n color: var(--zl-muted-foreground);\n font-size: .9375rem;\n display: flex;\n}\n\n.zl-hero__nav-links a {\n color: inherit;\n text-decoration: none;\n}\n\n.zl-hero__nav-links a:hover {\n text-decoration: underline;\n}\n\n.zl-hero__eyebrow {\n margin: 0 0 var(--zl-spacing-4);\n letter-spacing: .08em;\n text-transform: uppercase;\n color: var(--zl-muted-foreground);\n font-size: .8125rem;\n font-weight: 600;\n}\n\n.zl-hero__headline {\n margin: 0 0 var(--zl-spacing-4);\n font-family: var(--zl-font-family-heading);\n letter-spacing: -.02em;\n font-size: 2.75rem;\n font-weight: 700;\n line-height: 1.1;\n}\n\n.zl-hero__sub {\n margin: 0 0 var(--zl-spacing-8);\n max-width: 30rem;\n color: var(--zl-muted-foreground);\n font-size: 1.0625rem;\n line-height: 1.6;\n}\n\n.zl-hero__bullets {\n gap: var(--zl-spacing-2);\n margin: 0 0 var(--zl-spacing-8);\n flex-direction: column;\n padding: 0;\n list-style: none;\n display: flex;\n}\n\n.zl-hero__bullets li {\n align-items: center;\n gap: var(--zl-spacing-2);\n font-size: 1rem;\n display: flex;\n}\n\n.zl-hero__bullets li:before {\n content: \"✓\";\n color: var(--zl-success);\n font-weight: 700;\n}\n\n.zl-hero__footnote {\n padding-top: var(--zl-spacing-6);\n border-top: 1px solid var(--zl-border);\n color: var(--zl-muted-foreground);\n margin: 0;\n font-size: .8125rem;\n}\n\n.zl-hero__footnote strong {\n color: var(--zl-muted-foreground);\n font-weight: 600;\n}\n\n.zl-minimal {\n gap: var(--zl-spacing-6);\n flex-direction: column;\n width: min(24rem, 100%);\n margin-inline: auto;\n display: flex;\n}\n\n:host([variant=\"page\"]) .zl-minimal {\n padding: 52px 16px;\n}\n";
3950
+ var layout_chrome_default = ":host {\n color: var(--zl-foreground);\n font-family: var(--zl-font-family-sans);\n background: none;\n display: block;\n}\n\n:host([variant=\"page\"]) {\n background: var(--zl-background);\n min-height: 100%;\n}\n\nimg[data-zl-asset-broken] {\n display: none;\n}\n\n.zl-mount {\n min-height: var(--zl-page-min-height, auto);\n background: inherit;\n color: inherit;\n flex-direction: column;\n display: flex;\n container-type: inline-size;\n}\n\n:host([variant=\"page\"]) .zl-mount {\n min-height: var(--zl-page-min-height, 100vh);\n}\n\n.zl-mount > zl-page-shell, .zl-mount > [data-zl-template-root] {\n flex: auto;\n}\n\n.zl-attribution {\n background: none;\n justify-content: center;\n align-items: center;\n display: flex;\n}\n\n.zl-attribution[hidden] {\n display: none;\n}\n\n.zl-trustmark {\n gap: var(--zl-spacing-2-5);\n flex-wrap: wrap;\n}\n\n.zl-trustmark__mark {\n align-items: center;\n gap: var(--zl-spacing-2);\n font-family: var(--zl-font-family-sans);\n font-size: var(--zl-text-sm-size);\n line-height: var(--zl-text-sm-leading);\n color: var(--zl-foreground);\n text-decoration: none;\n display: inline-flex;\n}\n\n.zl-trustmark__mark:hover {\n text-underline-offset: 4px;\n text-decoration: underline;\n}\n\n.zl-trustmark__mark:focus-visible {\n outline: var(--zl-focus-width) solid var(--zl-ring);\n outline-offset: var(--zl-focus-offset);\n border-radius: var(--zl-radius-sm);\n}\n\n.zl-trustmark__logotype {\n flex-shrink: 0;\n width: 65px;\n height: 16px;\n line-height: 0;\n display: block;\n}\n\n.zl-trustmark__logotype-svg {\n width: 100%;\n height: 100%;\n display: block;\n}\n\n.zl-card-logo {\n width: auto;\n max-width: min(12rem, 60%);\n max-height: calc(var(--zl-card-logo-max-height, 2.5rem) * var(--zl-logo-scale, 1));\n object-fit: contain;\n}\n\n.zl-card-title {\n font-family: var(--zl-font-family-heading);\n font-size: var(--zl-text-xl-size);\n font-weight: var(--zl-font-weight-medium);\n color: var(--zl-card-foreground);\n text-align: left;\n margin: 0;\n line-height: 1;\n}\n\n.zl-card-subtitle {\n font-family: var(--zl-font-family-sans);\n font-size: var(--zl-text-sm-size);\n line-height: var(--zl-text-sm-leading);\n font-weight: var(--zl-font-weight-normal);\n color: var(--zl-muted-foreground);\n margin: 0;\n}\n\n.zl-field-group {\n gap: var(--zl-spacing-7);\n flex-direction: column;\n display: flex;\n}\n\n.zl-field-group:not(:has(*)) {\n display: none;\n}\n\n.zl-card-actions {\n gap: var(--zl-spacing-3);\n flex-direction: column;\n display: flex;\n}\n\n[data-suppress-header] .zl-card-title, [data-suppress-header] .zl-card-subtitle {\n clip-path: inset(50%);\n white-space: nowrap;\n border: 0;\n width: 1px;\n height: 1px;\n margin: -1px;\n padding: 0;\n position: absolute;\n overflow: hidden;\n}\n\n.zl-card-nav {\n font-family: var(--zl-font-family-sans);\n font-size: var(--zl-text-sm-size);\n line-height: var(--zl-text-sm-leading);\n font-weight: var(--zl-font-weight-normal);\n color: var(--zl-muted-foreground);\n text-align: center;\n margin: 0;\n}\n\n.zl-card-nav__link {\n font: inherit;\n line-height: inherit;\n color: var(--zl-link);\n text-underline-offset: 4px;\n cursor: pointer;\n background: none;\n border: none;\n margin: 0;\n padding: 0;\n text-decoration: underline;\n display: inline;\n}\n\n.zl-card-nav__link:hover {\n color: var(--zl-foreground);\n}\n\n.zl-card-nav__link:focus-visible {\n outline: var(--zl-focus-width) solid var(--zl-ring);\n outline-offset: var(--zl-focus-offset);\n}\n\n.zl-card-forgot {\n text-align: right;\n margin: 0;\n}\n\n.zl-card-forgot__link {\n font-family: var(--zl-font-family-sans);\n font-size: var(--zl-text-sm-size);\n line-height: var(--zl-text-sm-leading);\n font-weight: var(--zl-font-weight-normal);\n color: var(--zl-link);\n text-underline-offset: 4px;\n cursor: pointer;\n background: none;\n border: none;\n margin: 0;\n padding: 0;\n text-decoration: none;\n display: inline;\n}\n\n.zl-card-forgot__link:hover {\n text-decoration: underline;\n}\n\n.zl-card-forgot__link:focus-visible {\n outline: var(--zl-focus-width) solid var(--zl-ring);\n outline-offset: var(--zl-focus-offset);\n}\n\n.zl-split {\n grid-template-columns: var(--zl-split-columns, minmax(0, 1fr) minmax(0, 1fr));\n align-items: var(--zl-split-align, center);\n gap: var(--zl-spacing-8);\n width: 100%;\n min-height: 100%;\n display: grid;\n}\n\n.zl-split__brand {\n justify-content: center;\n align-items: center;\n gap: var(--zl-spacing-6);\n padding: var(--zl-spacing-8) var(--zl-spacing-4);\n flex-direction: column;\n display: flex;\n}\n\n.zl-split__logo {\n max-width: min(16rem, 60%);\n max-height: calc(var(--zl-split-logo-max-height, 16rem) * var(--zl-logo-scale, 1));\n}\n\n.zl-split__logo:has( + .zl-split__hero) {\n max-height: calc(var(--zl-split-logo-max-height, 8rem) * var(--zl-logo-scale, 1));\n}\n\n.zl-split__logo:has( + .zl-split__hero) + .zl-split__hero {\n max-height: min(var(--zl-split-hero-max-height, 22.5rem),\n max(6rem,\n calc(100vh - 104px - var(--zl-spacing-8) - var(--zl-spacing-8) -\n 2.5rem - var(--zl-spacing-6) - var(--zl-spacing-6) -\n calc(var(--zl-split-logo-max-height, 8rem) * var(--zl-logo-scale, 1)))));\n}\n\n.zl-split__hero {\n width: 100%;\n max-height: var(--zl-split-hero-max-height, min(22.5rem, 60vh));\n border-radius: var(--zl-radius-lg);\n object-fit: cover;\n}\n\n.zl-split__placeholder {\n aspect-ratio: 4 / 5;\n border-radius: var(--zl-radius-lg);\n background: radial-gradient(120% 120% at 18% 12%,\n var(--zl-gradient-lavender-start) 0%,\n transparent 60%),\n radial-gradient(130% 130% at 85% 92%,\n var(--zl-gradient-rose-start) 0%,\n transparent 60%),\n var(--zl-card);\n width: min(18rem, 85%);\n}\n\n.zl-split__form {\n justify-content: center;\n align-items: center;\n gap: var(--zl-spacing-6);\n flex-direction: column;\n padding: 16px;\n display: flex;\n}\n\n.zl-split__compact {\n max-width: min(12rem, 60%);\n display: none;\n}\n\nimg.zl-split__compact {\n max-height: calc(2.5rem * var(--zl-logo-scale, 1));\n}\n\nimg.zl-split__compact--hero {\n border-radius: var(--zl-radius-lg);\n object-fit: cover;\n width: 100%;\n max-width: 100%;\n max-height: 6rem;\n}\n\n.zl-split--right .zl-split__brand {\n order: 2;\n}\n\n.zl-split--right .zl-split__form {\n order: 1;\n}\n\nzl-page-shell:has(.zl-split) [slot=\"footer\"].zl-attribution {\n grid-template-columns: var(--zl-split-columns, minmax(0, 1fr) minmax(0, 1fr));\n gap: var(--zl-spacing-8);\n width: 100%;\n display: grid;\n}\n\nzl-page-shell:has(.zl-split) [slot=\"footer\"].zl-attribution > * {\n grid-column: 2;\n justify-self: center;\n}\n\nzl-page-shell:has(.zl-split--right) [slot=\"footer\"].zl-attribution > * {\n grid-column: 1;\n}\n\nzl-page-shell:has(.zl-split) [slot=\"footer\"].zl-attribution ::slotted(*) {\n grid-column: 2;\n justify-self: center;\n}\n\nzl-page-shell:has(.zl-split--right) [slot=\"footer\"].zl-attribution ::slotted(*) {\n grid-column: 1;\n}\n\n@container (width <= 48rem) {\n .zl-split {\n grid-template-columns: minmax(0, 1fr);\n }\n\n .zl-split__brand {\n display: var(--zl-split-brand-mobile, none);\n }\n\n .zl-split__compact {\n display: block;\n }\n\n zl-page-shell:has(.zl-split) [slot=\"footer\"].zl-attribution {\n grid-template-columns: minmax(0, 1fr);\n }\n\n zl-page-shell:has(.zl-split) .zl-attribution > *, zl-page-shell:has(.zl-split--right) [slot=\"footer\"].zl-attribution > * {\n grid-column: 1;\n }\n}\n\n.zl-hero {\n text-align: left;\n width: 100%;\n max-width: 34rem;\n color: var(--zl-foreground);\n font-family: var(--zl-font-family-sans);\n flex-direction: column;\n display: flex;\n}\n\n.zl-hero__nav {\n margin-bottom: var(--zl-spacing-8);\n justify-content: space-between;\n align-items: center;\n display: flex;\n}\n\n.zl-hero__brand {\n align-items: center;\n gap: var(--zl-spacing-2);\n letter-spacing: -.01em;\n font-size: 1.125rem;\n font-weight: 700;\n display: inline-flex;\n}\n\n.zl-hero__brand-logo {\n max-height: calc(1.75rem * var(--zl-logo-scale, 1));\n}\n\n.zl-hero__brand-mark {\n border-radius: var(--zl-radius-md);\n background: linear-gradient(135deg,\n var(--zl-gradient-lavender-start),\n var(--zl-gradient-rose-start));\n width: 1.75rem;\n height: 1.75rem;\n display: inline-flex;\n}\n\n.zl-hero__compact-brand {\n overflow-wrap: anywhere;\n letter-spacing: -.01em;\n max-width: 100%;\n color: var(--zl-foreground);\n margin: 0;\n font-size: 1.125rem;\n font-weight: 700;\n}\n\n.zl-hero__nav-links {\n gap: var(--zl-spacing-6);\n color: var(--zl-muted-foreground);\n font-size: .9375rem;\n display: flex;\n}\n\n.zl-hero__nav-links a {\n color: inherit;\n text-decoration: none;\n}\n\n.zl-hero__nav-links a:hover {\n text-decoration: underline;\n}\n\n.zl-hero__eyebrow {\n margin: 0 0 var(--zl-spacing-4);\n letter-spacing: .08em;\n text-transform: uppercase;\n color: var(--zl-muted-foreground);\n font-size: .8125rem;\n font-weight: 600;\n}\n\n.zl-hero__headline {\n margin: 0 0 var(--zl-spacing-4);\n font-family: var(--zl-font-family-heading);\n letter-spacing: -.02em;\n font-size: 2.75rem;\n font-weight: 700;\n line-height: 1.1;\n}\n\n.zl-hero__sub {\n margin: 0 0 var(--zl-spacing-8);\n max-width: 30rem;\n color: var(--zl-muted-foreground);\n font-size: 1.0625rem;\n line-height: 1.6;\n}\n\n.zl-hero__bullets {\n gap: var(--zl-spacing-2);\n margin: 0 0 var(--zl-spacing-8);\n flex-direction: column;\n padding: 0;\n list-style: none;\n display: flex;\n}\n\n.zl-hero__bullets li {\n align-items: center;\n gap: var(--zl-spacing-2);\n font-size: 1rem;\n display: flex;\n}\n\n.zl-hero__bullets li:before {\n content: \"✓\";\n color: var(--zl-success);\n font-weight: 700;\n}\n\n.zl-hero__footnote {\n padding-top: var(--zl-spacing-6);\n border-top: 1px solid var(--zl-border);\n color: var(--zl-muted-foreground);\n margin: 0;\n font-size: .8125rem;\n}\n\n.zl-hero__footnote strong {\n color: var(--zl-muted-foreground);\n font-weight: 600;\n}\n\n.zl-minimal {\n gap: var(--zl-spacing-6);\n flex-direction: column;\n width: min(24rem, 100%);\n margin-inline: auto;\n display: flex;\n}\n\n:host([variant=\"page\"]) .zl-minimal {\n padding: 52px 16px;\n}\n";
3508
3951
  //#endregion
3509
3952
  //#region src/orchestrator/zitadel-login.ts
3510
3953
  /** Narrow a rendered named element to the `formValue` field-atom contract. */
@@ -3635,6 +4078,13 @@ let ZitadelLogin = class ZitadelLogin extends ZitadelSurface {
3635
4078
  this.#_formValues_accessor_storage = value;
3636
4079
  }
3637
4080
  engine = null;
4081
+ /**
4082
+ * The theme the last commit rendered with. The template output carries the
4083
+ * resolved side's mark, so a theme change rewrites the string and
4084
+ * `unsafeHTML` rebuilds the form — which drops what the visitor had typed
4085
+ * unless it is put back.
4086
+ */
4087
+ lastRenderedTheme = null;
3638
4088
  sanitise = createSanitiser();
3639
4089
  /**
3640
4090
  * Cached compiled tenant template, keyed by source string. Re-rendering on
@@ -3733,9 +4183,14 @@ let ZitadelLogin = class ZitadelLogin extends ZitadelSurface {
3733
4183
  }
3734
4184
  for (const card of this.shadowRoot.querySelectorAll("zl-card")) card.toggleAttribute("data-suppress-header", this.suppressHeader);
3735
4185
  }
4186
+ const previousTheme = this.lastRenderedTheme;
4187
+ this.lastRenderedTheme = this.themeController.theme;
3736
4188
  const props = changed;
3737
- if (!props.has("response")) return;
3738
- this.hydrateStepAfterRender(props.get("response") == null);
4189
+ if (props.has("response")) {
4190
+ this.hydrateStepAfterRender(props.get("response") == null);
4191
+ return;
4192
+ }
4193
+ if (previousTheme !== null && previousTheme !== this.lastRenderedTheme) this.restoreValuesAfterRender();
3739
4194
  }
3740
4195
  /**
3741
4196
  * Apply captured values and move focus once the new step has fully
@@ -3750,11 +4205,19 @@ let ZitadelLogin = class ZitadelLogin extends ZitadelSurface {
3750
4205
  * swaps are user-initiated, so focus moves in both modes.
3751
4206
  */
3752
4207
  async hydrateStepAfterRender(initial = false) {
4208
+ await this.restoreValuesAfterRender();
4209
+ if (!initial || this.variant === "page") this.moveFocusToFirstField(initial && this.variant === "page");
4210
+ }
4211
+ /**
4212
+ * Put captured values back once the rebuilt subtree has rendered. Awaits the
4213
+ * child atoms' own first render before touching them, rather than guessing a
4214
+ * frame with `requestAnimationFrame`.
4215
+ */
4216
+ async restoreValuesAfterRender() {
3753
4217
  await this.updateComplete;
3754
4218
  const atoms = this.shadowRoot?.querySelectorAll("zl-field, zl-select, zl-checkbox, zl-button");
3755
4219
  if (atoms) await Promise.all(Array.from(atoms).map((atom) => atom.updateComplete));
3756
4220
  this.applyValuesToFields();
3757
- if (!initial || this.variant === "page") this.moveFocusToFirstField(initial && this.variant === "page");
3758
4221
  }
3759
4222
  render() {
3760
4223
  if (this.startupError) return html`<form class="zl-mount" novalidate>
@@ -3796,6 +4259,20 @@ let ZitadelLogin = class ZitadelLogin extends ZitadelSurface {
3796
4259
  return rendered + html;
3797
4260
  }
3798
4261
  /**
4262
+ * The branding a template renders against, with `logo_url` already resolved
4263
+ * to the mark for the active side. Templates read one logo field and get the
4264
+ * right file per surface; the per-side URLs stay on `theme` for a template
4265
+ * that wants to reach them itself.
4266
+ */
4267
+ brandingForTemplate() {
4268
+ if (!this.branding) return {};
4269
+ const logoUrl = resolveLogoUrl(this.branding, this.themeController.theme);
4270
+ return {
4271
+ ...this.branding,
4272
+ logo_url: logoUrl
4273
+ };
4274
+ }
4275
+ /**
3799
4276
  * "Secured with Zitadel" attribution chrome injected into every
3800
4277
  * template's page-shell footer slot. Controlled by
3801
4278
  * `branding.attribution.show_zitadel` — defaults to `true` for
@@ -3876,7 +4353,11 @@ let ZitadelLogin = class ZitadelLogin extends ZitadelSurface {
3876
4353
  this.armed = true;
3877
4354
  } else if (!hasBack && this.armed) {
3878
4355
  this.armed = false;
3879
- if (history.state?.zl === true) {
4356
+ if (history.state?.zl === true) if (this.completing) history.replaceState({
4357
+ ...history.state,
4358
+ zl: false
4359
+ }, "");
4360
+ else {
3880
4361
  this.ignoreNextPop = true;
3881
4362
  history.back();
3882
4363
  }
@@ -3948,7 +4429,7 @@ let ZitadelLogin = class ZitadelLogin extends ZitadelSurface {
3948
4429
  messages: [],
3949
4430
  identity: this.deriveIdentity(),
3950
4431
  errors,
3951
- branding: this.branding ?? {},
4432
+ branding: this.brandingForTemplate(),
3952
4433
  loading: this.loading
3953
4434
  };
3954
4435
  let raw;
@@ -4612,7 +5093,7 @@ let ZitadelLogout = class ZitadelLogout extends LitElement {
4612
5093
  const root = this.shadowRoot;
4613
5094
  if (root && !this.templateMode) {
4614
5095
  applyBaseTokens(root);
4615
- applyBrandingTokens(root, void 0, this.themeController.theme);
5096
+ applyBrandingTokens(root, void 0);
4616
5097
  }
4617
5098
  this.dataset.theme = this.themeController.theme;
4618
5099
  this.toggleAttribute("data-theme-dark", this.themeController.theme === "dark");
@@ -5096,6 +5577,6 @@ __decorate([state()], ZitadelSession.prototype, "loading", null);
5096
5577
  __decorate([state()], ZitadelSession.prototype, "errorMessage", null);
5097
5578
  ZitadelSession = __decorate([customElement("zitadel-session")], ZitadelSession);
5098
5579
  //#endregion
5099
- export { submitStep as C, startFlow as S, zitadelTrustmarkInnerHtml as T, MANDATORY_GATES_MARKER as _, TEMPLATE_NAMES as a, validateBranding as b, applyBrandingTokens as c, createSanitiser as d, builtinLocales as f, en as g, de as h, layout_chrome_default as i, buildBrandingStylesheet as l, it as m, ZitadelLogout as n, ThemeController as o, businessLocales as p, ZitadelLogin as r, applyFontUrl as s, ZitadelSession as t, resolveTheme as u, mandatoryGatesMarkerComment as v, ZITADEL_ATTRIBUTION_LOGOTYPE_SVG as w, getCurrentStep as x, patchMandatoryGates as y };
5580
+ export { getCurrentStep as C, zitadelTrustmarkInnerHtml as D, ZITADEL_ATTRIBUTION_LOGOTYPE_SVG as E, validateBranding as S, submitStep as T, MANDATORY_GATES_MARKER as _, TEMPLATE_NAMES as a, publishedSides as b, applyBrandingTokens as c, createSanitiser as d, builtinLocales as f, en as g, de as h, layout_chrome_default as i, buildBrandingStylesheet as l, it as m, ZitadelLogout as n, ThemeController as o, businessLocales as p, ZitadelLogin as r, applyFontUrl as s, ZitadelSession as t, resolveTheme as u, mandatoryGatesMarkerComment as v, startFlow as w, resolveLogoUrl as x, patchMandatoryGates as y };
5100
5581
 
5101
- //# sourceMappingURL=orchestrator-BPX0Uc77.mjs.map
5582
+ //# sourceMappingURL=orchestrator-BVmKaCqz.mjs.map