// NO PROD JSON API (bucket B — feed is a DataTables render payload or
//   {"html":...}, not data) — mock until backend (see PROD_API_INVENTORY.md)
// Represents: GET /skins/ + GET /skins/{id}/{tab} · SkinsController — see docs/ISYSTEM_REFERENCE.md §Batch 6 "Skins deep-dive — part 1/3"
/* CMS ▾ → Skins. THE consolidated skin editor — the §8.2 reconciliation of the prototype's three
   skin surfaces into the one real screen (docs/UIUX_ELEVATION_BRIEF.md §8.2).

   ── What this file replaces ────────────────────────────────────────────────────────────────────
   · src/pages/HostSkins.jsx — the wired `CMS → Skins` target (route key `host-skins`, /cms/skins).
     22 label-string tabs, list = ID/Name/Currency table. Loads BEFORE this file, so the `HostSkins`
     component below deliberately shadows its same-named global (same trick HostSetVendorsGroups.jsx
     uses on SetVendorsGroups). That file is not edited and stays on disk until the orchestrator
     drops its <script> tag.
   · src/pages/Skins.jsx — the nicer 19-tab pill-tab redesign reachable only through the orphaned
     `brands` route (/brands). Its SHAPE is the base for the editor shell here (pill tabs, per-tab
     subtitles, grouped forms, back-arrow header). Its file is NOT edited and NOT imported: every
     name here is re-expressed under the Hsk/HSK/hsk prefix.
   · The stale comment at src/host.jsx:85-86 claiming the per-skin Customer.io / Third-Party config
     "lives" behind CMS → Skins. It does not — see the Customer.io note below.

   ── Protected surfaces this file does NOT touch ───────────────────────────────────────────────
   · `renderSettings()` inside Skins.jsx (brief §4.4) — the prototype-only 8-toggle "Operational
     settings" panel (registration, KYC-on-signup, age gate, geo-blocking, maintenance mode, RTL,
     dark-default, cookie banner). It is a closure inside `SkinDetail`, NOT a top-level/window
     global, so it is unreachable from here — nothing can call it. It is therefore neither copied
     nor reimplemented: HskTabRegistry["operational-settings"] renders the ported tab (IWAKIRI-NATIVE)
     (HskProtectedSettingsTab) and the Settings tab body links to it. None of those 8 toggles exist
     in SkinsController, so none of them appear anywhere in this file.
     IMPORTANT: the real platform's `Settings` tab (tab #2, /skins/{id}/settings/) is a DIFFERENT
     screen — the 130-row Setting/Value table documented in Batch 6 part 1 §4 — and IS implemented
     here (HskSettingsTab). Same word, two different surfaces; see phaseD/skins-1.md.
   · The Customer.io / CDP panel lives in the protected src/pages/Settings.jsx (brief §4.1,
     `CustomerioPanel` / `NotificationChannelsPanel`). The real platform does have a per-skin
     Customer.io tab (#3, /skins/{id}/customerio/), so the tab EXISTS here — as an honest pointer
     (HskCustomerIoTab), never a copy. The legacy HostSkins.jsx rendered those protected panels
     under a "Third Party Integration" tab; that duplication is deliberately dropped.

   ── Real screen, in one paragraph ─────────────────────────────────────────────────────────────
   `GET /skins/` → SkinsController::index (routes/admin.php:108-110, SC:543-558) aborts 404 unless
   isadmin(); its DataTable feed `GET /getskins/` → getSkinsListTable (SC:4047-4130) additionally
   scopes non-superadmins to $user->getSkinIDS(). Columns are ID / Name / Actions only. Detail:
   `GET /skins/{id}/` → showSkinDetails (SC:987-1004) renders admin.skins.skin inside
   resources/views/admin/skins/template.blade.php, whose tab strip (template.blade.php:63-241) is
   the authoritative 22-tab list encoded in HSK_TABS below. Most tabs save through the one shared
   endpoint `POST /skins/saveSkin/{id}/{tab}` → saveEditSkin (SC:2789) — so Save is PER TAB, not a
   single page-level Save (a deliberate difference from Skins.jsx's global save button).

   ── Known real-platform defects, handled per the known-bug policy (CLAUDE.md) ─────────────────
   · List sorting: getSkinsListTable's order-by switch has cases "id" and "username" (SC:4083-4098)
     but the column is registered as `name` (SC:551-555) — sorting by Name never fires; server
     default is skins.id ASC. Evident intent implemented (Name is sortable here).
   · New Skin / Home save: an empty `skin_code` writes its error into $errors["name"] instead of
     $errors["skin_code"] (SC:3958, SC:2822), so the message lands under the wrong field. Evident
     intent implemented (the error renders under Code).
   · Settings tab: the "Meta description" textarea is named `meta_title` (settings.blade.php:827),
     duplicating the Meta Title input — the textarea overwrites the title on submit and
     skins.meta_description is nulled on every save. Evident intent implemented (a real
     `meta_description` field).
   · Registration: the only trigger (the "User Registration Form" row) is commented out
     (settings.blade.php:23-35) AND `GET /skins/{id}/settings/registration` maps to
     SkinsController@showSkinRegistrationSettings, a method that does not exist in this branch —
     only the POST half survives. Evident intent implemented: the row + a working editor, with the
     breakage stated in the UI rather than hidden.
   All four divergences are also flagged as SUGGESTION lines at their implementation site.

   <!-- SUGGESTION: fix getSkinsListTable's order-by switch — case "username" should be case "name" — so the Name column actually sorts instead of silently falling back to skins.id ASC. -->
   <!-- SUGGESTION: key the empty-skin_code validation error to "skin_code" in both saveNewSkin (SC:3958) and saveEditSkin case "home" (SC:2822); today it renders under the Name field. -->
   <!-- SUGGESTION: rename the Meta description textarea to `meta_description` (settings.blade.php:827) — as named it clobbers Meta Title and nulls skins.meta_description on every settings save. -->
   <!-- SUGGESTION: restore SkinsController::showSkinRegistrationSettings (the GET half of /skins/{id}/settings/registration) and un-comment the "User Registration Form" row, or delete the dead route, view and POST handler outright. The registration config is currently unreachable from the UI while its POST endpoint stays live. -->
   <!-- SUGGESTION: drop the literal debug strings ("params" => "test", "record_data" => "testetttttttt", SC:1236) from editSkinRegistrationSettings' success payload. -->

   ── Part 2 hand-off ───────────────────────────────────────────────────────────────────────────
   Tab bodies are looked up in `HskTabRegistry` AT RENDER TIME, so a later-loading file
   (src/pages/HostSkinsUnifiedTabs.jsx) fills the remaining tabs by assigning into the same object
   without touching this shell. Full contract in phaseD/skins-1.md; short version:
       HskTabRegistry["graphic"] = Hsk2GraphicTab;   // a React COMPONENT, not a render function
   Every unregistered tab id falls back to HskTabPending. */

const { useState: hskUseState, useMemo: hskUseMemo, useEffect: hskUseEffect } = React;

/* Deterministic PRNG (FNV-1a + mulberry32) — same convention as the sibling Host pages so the mock
   list renders identically on every load. */
const hskHash = (s) => { let h = 2166136261 >>> 0; for (let i = 0; i < s.length; i++) { h ^= s.charCodeAt(i); h = Math.imul(h, 16777619); } return h >>> 0; };
const hskRng = pbRng;   // was a local copy of mulberry32 — see pbRng in src/data.jsx
const hskToast = (title, detail) => (window.hrsToast ? window.hrsToast(title, detail) : (window.PAYBO?.emitToast && window.PAYBO.emitToast({ id: `hsk-${Date.now()}`, tx_id: title, amount: 0, currency: "CMS", player: "Skins", reason: detail || "" })));

/* ══════════════════════════════════════════════════════════════════════════════════════════════
   1. Option sources — every list below mirrors a real helper/table named in the reference.
   ══════════════════════════════════════════════════════════════════════════════════════════ */

/* currencies() → DB currencies_list, key = currency code (utils.php:2014-2035). Kept aligned with
   the operator's live currency set (see HostSetCurrencies.jsx). */
const HSK_CURRENCIES = ["ARS", "BOB", "BRL", "CLP", "COP", "EUR", "GBP", "LBP", "MXN", "PEN", "PYG", "USD", "UYU", "VES"];

/* LanguagesController::getLanguages() → DB `languages`. */
const HSK_LANGUAGES = ["Español", "English", "Português", "Português-Brasil", "Italiano", "Deutsche", "Français", "Türkçe", "Arabic", "Română", "Chinese", "Magyar"];

/* timezones() (utils.php:1587). */
const HSK_TIMEZONES = [
  "(GMT-8:00) America/Los_Angeles", "(GMT-5:00) America/Bogota", "(GMT-5:00) America/Lima",
  "(GMT-4:00) America/Caracas", "(GMT-4:00) America/La_Paz", "(GMT-4:00) America/Santiago",
  "(GMT-3:00) America/Argentina/Buenos_Aires", "(GMT-3:00) America/Sao_Paulo", "(GMT-3:00) America/Asuncion",
  "(GMT-3:00) America/Montevideo", "(GMT+0:00) Europe/London", "(GMT+1:00) Europe/Madrid",
  "(GMT+1:00) Europe/Rome", "(GMT+2:00) Asia/Beirut", "(GMT+3:00) Africa/Nairobi", "(GMT+4:00) Asia/Dubai",
];

/* countries() (utils.php:1310) — ISO ⇄ name. Also feeds the registration editor's authorized-country
   list, which keys its checkboxes country-{ISO}. */
const HSK_COUNTRIES = [
  ["AR", "Argentina"], ["BO", "Bolivia"], ["BR", "Brazil"], ["CL", "Chile"], ["CO", "Colombia"],
  ["CR", "Costa Rica"], ["CW", "Curaçao"], ["DE", "Germany"], ["EC", "Ecuador"], ["ES", "Spain"],
  ["FR", "France"], ["GB", "United Kingdom"], ["GT", "Guatemala"], ["HN", "Honduras"], ["IT", "Italy"],
  ["KE", "Kenya"], ["LB", "Lebanon"], ["MX", "Mexico"], ["NI", "Nicaragua"], ["PA", "Panama"],
  ["PE", "Peru"], ["PY", "Paraguay"], ["RO", "Romania"], ["SV", "El Salvador"], ["TR", "Türkiye"],
  ["UY", "Uruguay"], ["VE", "Venezuela"],
];

/* CryptoIo::cryptoio_coins() → DB table currencies_cryptoio (CryptoIo.php:124-138). One settings row
   per coin, named cryptoio[{COIN}], stored serialized in skins.cryptoio_addresses. */
const HSK_CRYPTOIO_COINS = ["BTC", "ETH", "LTC", "USDT", "TRX", "DOGE"];

/* getSkinsUsersByCategry(SHOP) — the skin's own SHOP-level users, feeding `online_shop_id`. */
const HSK_SHOPS = [["12", "Online Shop"], ["48", "Shop Centro"], ["91", "Shop Norte"], ["104", "Shop Retail"]];

/* PromotionsController::getPromoSkin($id) — the skin's promotions, feeding `default_promo`. */
const HSK_PROMOS = [["11", "Welcome 100%"], ["18", "Recarga Viernes"], ["24", "Cashback Semanal"]];

/* Active MobileValidator rows (app/Services/MobileValidators/ — Safaricom / Airtel / Taifa / Geez /
   Dotgo / Zynle). Consumed only by the registration editor. */
const HSK_VALIDATORS = [["1", "Safaricom"], ["2", "Airtel"], ["3", "Taifa"], ["4", "Geez"], ["5", "Dotgo"], ["6", "Zynle"]];

/* ══════════════════════════════════════════════════════════════════════════════════════════════
   2. The authoritative tab list — template.blade.php:63-241, in display order.
      22 tabs render for a super admin. A 23rd ("Tags", /skins/{id}/tags/) is commented out in the
      Blade (L126-132) and therefore NOT rendered here either.
      `scoped: true` marks the three tabs a scoped Customer-Care manager can still see
      ($scopedManager = !isadmin() && isCustomCare(), L65-70, hides everything else).
      `part` records who owns the body: 1 = this file, 2 = HostSkinsUnifiedTabs.jsx.
   ══════════════════════════════════════════════════════════════════════════════════════════ */
const HSK_TABS = [
  { id: "home", label: "Home", slug: "", icon: "dashboard", part: 1,
    sub: "Identity and locale — the six fields that define the skin, plus the jackpot master switch.",
    route: "GET /skins/{id}/", save: "POST /skins/saveSkin/{id}/home", gate: "isadmin()" },
  { id: "settings", label: "Settings", slug: "settings", icon: "settings", part: 1,
    sub: "The platform's single Setting / Value table — feature flags, integration credentials and per-skin values.",
    route: "GET /skins/{id}/settings/", save: "POST /skins/saveSkin/{id}/settings", gate: "isadmin()" },
  { id: "customerio", label: "Customer.io", slug: "customerio", icon: "mail", part: 1,
    sub: "Per-skin Customer.io / CDP configuration.",
    route: "GET /skins/{id}/customerio/", save: "POST /skins/saveSkin/{id}/customerio", gate: "none — showTab applies no role check to this tab" },
  { id: "sidebar", label: "Sidebar", slug: "sidebar", icon: "list", part: null,
    sub: "Player-site sidebar entries for this skin.",
    route: "GET /skins/{id}/sidebar/", save: "POST /skins/saveSkin/{id}/sidebar", gate: "none — showTab applies no role check to this tab" },
  { id: "providers", label: "Provider", slug: "providers", icon: "globe", part: 2, scoped: true,
    sub: "Per-skin provider matrix — Active / Banner / Featured / Visible / Priority.",
    route: "GET /skins/{id}/providers/", save: "POST /skins/saveSkin/{id}/providers", gate: "canManageSkin(id,'support_skin_providers')" },
  { id: "gamemanagement", label: "Games", slug: "games", icon: "grid", part: 2, scoped: true,
    sub: "Game grid for this skin — enable/disable, subcategories, labels and priority.",
    route: "GET /skins/{id}/gamemanagement/", save: "inline GET writers + 3 bulk POSTs (no saveEditSkin case)", gate: "canManageSkin(id,'support_skin_gamemanagement')" },
  { id: "subcategories", label: "Subcategories", slug: "subcategories", icon: "tag", part: 2, scoped: true,
    sub: "Per-subcategory game ordering and the subcategory tile image.",
    route: "GET /skins/{id}/subcategoriesmanagement/", save: "GET updategamepriority + POST uploadImage / removeImage", gate: "canManageSkin(id,'support_skin_subcategories')" },
  { id: "graphic", label: "Graphic", slug: "graphic", icon: "star", part: 2,
    sub: "Logo, black logo, favicon, footer image, template/header/footer versions, colours and custom scripts.",
    route: "GET /skins/{id}/graphic/", save: "POST /skins/saveSkin/{id}/graphic", gate: "isadmin()" },
  { id: "domains", label: "Domains", slug: "domains", icon: "globe", part: 2,
    sub: "Hostnames pointing at this skin, and which one is main.",
    route: "GET /skins/{id}/domains/", save: "POST /skins/saveSkin/{id}/domains", gate: "isadmin()" },
  { id: "jackpot", label: "Jackpot", slug: "jackpot", icon: "crown", part: 2,
    sub: "The three jackpot pots — only rendered while the Home tab's Jackpot switch is on.",
    route: "GET /skins/{id}/jackpot/", save: "POST /skins/saveSkin/{id}/jackpot", gate: "isadmin()" },
  { id: "levels", label: "Levels", slug: "levels", icon: "chart", part: null,
    sub: "Per-skin player levels.",
    route: "GET /skins/{id}/levels/", save: "POST /skins/saveSkin/{id}/levels", gate: "none — showTab applies no role check to this tab" },
  { id: "banner-provider-promotion", label: "Banner Provider Promotion", slug: "banner-provider-promotion", icon: "flag", part: null,
    sub: "Provider-promotion banners for this skin.",
    route: "GET /skins/{id}/banner_provider_promotion/", save: "POST /skins/saveSkin/{id}/banner_provider_promotion", gate: "none — showTab applies no role check to this tab" },
  { id: "deposit-methods", label: "Deposit methods", slug: "deposit-methods", icon: "arrow_down", part: 2,
    sub: "Which deposit methods this skin offers, with min/max and day/week/month limits.",
    route: "GET /skins/{id}/depositmethods/", save: "POST /skins/saveSkin/{id}/depositmethods", gate: "isadmin()" },
  { id: "withdrawal-methods", label: "Withdrawal methods", slug: "withdrawal-methods", icon: "arrow_up", part: 2,
    sub: "Which withdrawal methods this skin offers, with min/max and day/week/month limits.",
    route: "GET /skins/{id}/withdrawmethods/", save: "POST /skins/saveSkin/{id}/withdrawmethods", gate: "isadmin()" },
  { id: "limits", label: "Limits", slug: "limits", icon: "sliders", part: 2,
    sub: "Per-vertical balance limits, GGR-limit switches and the payment reminder note.",
    route: "GET /skins/{id}/limits/", save: "POST /skins/saveSkin/{id}/limits", gate: "isadmin()" },
  { id: "homepage-view", label: "Homepage View (API)", slug: "homepage-view", icon: "dashboard", part: 2,
    sub: "The ordered block layout of the player-facing homepage.",
    route: "GET /skins/{id}/homepage_view/", save: "POST /skins/{id}/homepage_view (+ /reset)", gate: "isadmin() on show only — save/reset/games carry no role check" },
  { id: "faq-footer", label: "FAQ & FOOTER (API)", slug: "faq-footer", icon: "help", part: 2,
    sub: "FAQ and footer entries in 12 languages (English required).",
    route: "GET /skins/{id}/entries/", save: "POST / PUT / DELETE /skins/{id}/entries/{type}", gate: "none on any of the four actions" },
  { id: "footer", label: "FOOTER (API)", slug: "footer", icon: "list", part: 2,
    sub: "Footer navigation tree plus the per-language ownership / copyright lines.",
    route: "GET /skins/{id}/footer/", save: "POST /skins/{id}/footer/save + /footer/texts/save", gate: "isadmin() (but /footer/json is ungated)" },
  { id: "seo-content-blocks", label: "SEO Content Blocks", slug: "seo-content-blocks", icon: "search", part: null,
    sub: "Translated SEO copy blocks for this skin.",
    route: "GET /skins/{id}/seo-content-blocks/", save: "POST /skins/saveSkin/{id}/seo-content-blocks", gate: "none — showTab applies no role check to this tab" },
  { id: "sport", label: "Sport", slug: "sport", icon: "zap", part: 2,
    sub: "Sportsbook behaviour — copy printing, taxes, profit formula and open-bet cancellation.",
    route: "GET /skins/{id}/sport/", save: "POST /skins/saveSkin/{id}/sport", gate: "SkinPolicy::update → isadmin()" },
  { id: "financial", label: "Financial", slug: "financial", icon: "percent", part: 2,
    sub: "Who may move money to whom — a per-role matrix of inverted disable flags.",
    route: "GET /skins/{id}/financial/", save: "posts to the sport endpoint (real-platform bug)", gate: "SkinPolicy::update → isadmin() || (isSkinAdmin() && own skin)" },
  { id: "security", label: "Security", slug: "security", icon: "shield", part: 2,
    sub: "Which roles may change another user's password.",
    route: "GET /skins/{id}/security/", save: "POST /skins/saveSkin/{id}/security", gate: "SkinPolicy::update → isadmin() || Skin::allowChangePassword(level)" },

  /* IWAKIRI-NATIVE — no isystem counterpart. Every other tab above mirrors a real
     platform tab; this one does not, and that is deliberate. Iwakiri is becoming its
     own platform, so isystem's 22 tabs are a floor, not a ceiling. These 8 toggles are
     a ClickUp-specced feature (brief §4.4), ported verbatim from Skins.jsx's protected
     renderSettings() when the `brands` route was retired, so the feature survives the
     §8.2 consolidation. Traceability checks should treat IWAKIRI-NATIVE as intentional
     rather than flagging it as an untraceable invention. */
  { id: "operational-settings", label: "Operational settings", slug: "operational-settings", icon: "settings", part: 1,
    native: true,
    sub: "Player-facing site behaviour. IWAKIRI-NATIVE — no isystem counterpart.",
    route: "n/a — Iwakiri-native", save: "n/a — Iwakiri-native (prototype state only)", gate: "n/a" },
  /* TWO-FACTOR — the per-skin 2FA policy (spec §6). IWAKIRI-NATIVE in the same
     sense as operational-settings: no isystem counterpart today, because this is
     being specced FOR isystem rather than copied from it. Unlike that tab it is
     not prototype state — it writes real rows through set_skin_2fa_switch.

     NOT the `security` tab, which already exists and is something else entirely:
     security.blade.php is the allow_change_password matrix. Reusing that id
     would have silently replaced a real upstream screen with this one, and the
     name gate caught exactly that. */
  /* REGISTRATION lives INSIDE the Settings tab (owner ask, 2026-08-16 — it
     briefly had a tab of its own). One surface, not two: the builder is
     Hsk2RegistrationTab, rendered at the top of HskSettingsTab.
     docs/proposals/REGISTRATION_FORM.md carries the why. */
  /* TWO-FACTOR lives INSIDE the Security tab (owner ask, 2026-08-16 — it
     briefly had a tab of its own). Hsk2TwoFactorTab renders at the bottom of
     Hsk2SecurityTab; NOT the same thing as that tab's password matrix, which is
     the upstream allow_change_password screen. */
];

const HSK_TAB_ROUTES = HSK_TABS.map(t => [t.id, t.label, t.slug]);
const HSK_TAB_BY_ID = Object.fromEntries(HSK_TABS.map(t => [t.id, t]));

/* ══════════════════════════════════════════════════════════════════════════════════════════════
   3. Settings tab data — settings.blade.php in display order.
      (A) 40 presence toggles from skinSett() (SC:671-714 + SkinSetting::LABELS).
      (B) 6 game-visibility toggles from UsersController::GamesPermissions() (UC:2497-2508).
      (C) value rows 47-133, with the five section headers the Blade actually renders.
      Storage note per row is shown in the UI — these are the real column / table targets.
   ══════════════════════════════════════════════════════════════════════════════════════════ */
const HSK_SKIN_SETT = [
  ["enable_seo", "Indexing on search engines"],
  ["deposit_online", "Online deposit"],
  ["reg_free", "Free registration"],
  ["disable_registration", "Disable registration"],
  ["enable_goldenrace_virtual_header", "Enable GoldenRace Virtual Header"],
  ["enable_change_domain", "Enable Change Domain"],
  ["enable_custom_crashgames", "Enable Custom Crashgames Page"],
  ["show_all_games_casino", "Show All Games as default in Casino"],
  ["show_all_games_casinolive", "Show All Games as default in Casino Live"],
  ["enable_tt_race", "Enable TT Race"],
  ["enable_wire_custom_wallet", "Enable Wire Custom Wallet"],
  ["enable_tt_jackpot", "Enable TT Jackpot"],
  ["enable_pp_freegames", "Enable Promoplay Freegames"],
  ["enable_pp_shop", "Enable Promoplay Shop"],
  ["enable_pp_missions", "Enable Promoplay Missions"],
  ["enable_pp_insite_notifications", "Enable Promoplay In-Site Notifications"],
  ["fast_player", "Fast player"],
  ["force_login", "Login required"],
  ["enable_export", "Enable export (Users)"],
  ["disable_bet_section", "Disable BET section"],
  ["enable_agents_operators", "Enable agents operators"],
  ["enable_sportsbook_balance_limits", "Enable Sportsbook Balance Limits Dashboard"],
  ["enable_cms", "CMS"],
  ["enable_commissions_management", "Commissions Management"],
  ["enable_savagetech", "Enable Savagetech"],
  ["send_sport_bets_savagetech", "Send Sport Bets Savagetech"],
  ["disable_change_comm_profile_limits", "Disable Change commission profile limits"],
  ["enable_promotions_management", "Enable Promotions section"],
  ["enable_withdrawable_deposit", "Enable Withdrawable deposit"],
  ["onaim_enabled", "Enable OnAim gamification integration"],
  ["enable_user_unblock", "Enable User unblock"],
  ["disable_subnet_crud", "Disable Subnet Create/Edit"],
  ["disable_players_crud", "Disable Players Create/Edit"],
  ["can_calc_standard", "Enable standard commission calculation"],
  ["can_provv_cobanco", "Enable Cobanco"],
  ["disable_language_select", "Disable language selection"],
  ["separate_vip_casino_providers", "Separate VIP Casino providers"],
  ["need_email_confirm", "Email confirmation required"],
  ["sport_page_sidebar_hidden", "Hide Sport Page Sidebar"],
  ["relaxed_password_policy", "Relaxed password policy (registration: min 6 chars, no uppercase/special required)"],
];

const HSK_GAME_SETT = [
  ["show_sport", "Enable Sport"],
  ["show_casino", "Enable Casino"],
  ["show_casinolive", "Enable Casino Live"],
  ["show_poker", "Enable Poker"],
  ["show_virtual", "Enable Virtual"],
  ["show_lottery", "Enable Lottery"],
];

/* Rows are { l:label, n:input name, t:type, o:options, ph:placeholder, hint, store } — or
   { h:"section header" }. `inferred: true` = the Blade renders a backend.* key that resolves
   nowhere (runtime storage/lang is gitignored), so the label here is written for operators.
   `t` ∈ text | textarea | select | check. */
const HSK_VALUE_ROWS = [
  { l: "CF ZONE ID", n: "cf_zone_id", t: "text", store: "skins.custom_settings['cf_zone_id']" },
  { l: "Daily withdrawal limit", n: "withdrawal_daily_limit", t: "text", store: "skins.withdrawal_daily_limit", inferred: true, hint: "Cast to float on save." },
  { l: "Backoffice URL", n: "backoffice_url", t: "text", store: "skins.backoffice_url", inferred: true },
  { l: "PromoPlay API Url", n: "pp_api_url", t: "text", store: "skins.pp_api_url" },
  { l: "PromoPlay Username", n: "pp_username", t: "text", store: "skins.pp_username" },
  { l: "PromoPlay Public Key", n: "pp_public_key", t: "text", store: "skins.pp_public_key" },
  { l: "PromoPlay Secret Key", n: "pp_secret_key", t: "text", store: "skins.pp_secret_key" },
  { l: "Game List Scrollability (API)", n: "game_list_scroll_type", t: "select", store: "skins.game_list_scroll_type",
    o: [["infinite_scroll", "Infinite Scroll"], ["load_more", "Load More Button"]] },
  { l: "Skin default promotion", n: "default_promo", t: "select", store: "skins.default_promo", inferred: true, o: "promos" },
  { l: "Menu Display Mode (API)", n: "menu_display_mode", t: "select", store: "skins.menu_display_mode",
    o: [["expandable1", "expandable1"], ["expandable2", "expandable2"], ["tabs", "tabs"], ["classic", "classic"]] },
  { l: "Providers Display Mode (API)", n: "provider_display_mode", t: "select", store: "skins.provider_display_mode",
    o: [["slider", "slider"], ["expand", "expand"], ["arrows", "arrows"]] },
  { l: "Subcategories Display Mode (API)", n: "subcategory_display_mode", t: "select", store: "skins.subcategory_display_mode",
    o: [["slider", "slider"], ["arrows", "arrows"]] },
  { l: "TimelessTech Operator ID", n: "tt_operatorID", t: "text", store: "skins.tt_operatorID" },
  { l: "TimelessTech Entity ID", n: "tt_entityID", t: "text", store: "skins.tt_entityID" },
  { l: "TimelessTech Api URL", n: "tt_api_url", t: "text", store: "skins.tt_api_url" },
  { l: "TimelessTech Secret Key", n: "tt_secretkey", t: "text", store: "skins.tt_secretkey" },

  { h: "Payment Gateway Credentials (leave empty to use default sandbox)" },
  { l: "Payment API URL", n: "payment_api_url", t: "text", ph: "https://tltpay.io", store: "skins.payment_api_url" },
  { l: "Payment Merchant ID", n: "payment_merchant_id", t: "text", ph: "e.g. juegojoker", store: "skins.payment_merchant_id" },
  { l: "Payment Merchant Secret", n: "payment_merchant_secret", t: "text", store: "skins.payment_merchant_secret" },

  { h: "Cripten Flash Payment Provider" },
  { l: "Cripten Enabled", n: "cripten_enabled", t: "check", store: "skin_cripten_configs.enabled" },
  { l: "Cripten Environment", n: "cripten_environment", t: "select", store: "skin_cripten_configs.environment",
    o: [["sandbox", "sandbox"], ["production", "production"]] },
  { l: "Cripten Base URL", n: "cripten_base_url", t: "text", ph: "https://sandbox.cripten.io", store: "skin_cripten_configs.base_url", hint: "Empty falls back to the env default." },
  { l: "Cripten Client ID", n: "cripten_client_id", t: "text", store: "skin_cripten_configs.client_id" },
  { l: "Cripten Client Secret", n: "cripten_client_secret", t: "text", store: "skin_cripten_configs.client_secret" },
  { l: "Cripten Merchant Secret", n: "cripten_merchant_secret", t: "text", store: "skin_cripten_configs.merchant_secret", hint: "Used for callback MD5 + JWT signing." },
  { l: "Cripten Callback URL", n: "cripten_callback_url", t: "text", store: "skin_cripten_configs.callback_url", hint: "Optional override." },

  { h: "Mercurio Payment Provider" },
  { l: "Mercurio Enabled", n: "mercurio_enabled", t: "check", store: "skin_mercurio_configs.enabled" },
  { l: "Mercurio Environment", n: "mercurio_environment", t: "select", store: "skin_mercurio_configs.environment",
    o: [["sandbox", "sandbox"], ["production", "production"]] },
  { l: "Mercurio Domain (DOMAIN)", n: "mercurio_domain", t: "text", store: "skin_mercurio_configs.domain", hint: "Empty falls back to the env default." },
  { l: "Mercurio Payment Page Domain", n: "mercurio_payment_page_domain", t: "text", store: "skin_mercurio_configs.payment_page_domain" },
  { l: "Mercurio Merchant ID", n: "mercurio_merchant_id", t: "text", store: "skin_mercurio_configs.merchant_id", hint: "Optional." },
  { l: "Mercurio API Key", n: "mercurio_api_key", t: "text", store: "skin_mercurio_configs.api_key", hint: "x-token + api_secret + callback HMAC signing key." },
  { l: "Mercurio Callback Secret", n: "mercurio_callback_secret", t: "text", store: "skin_mercurio_configs.callback_secret", hint: "Optional; defaults to the API Key." },
  { l: "Mercurio Config JSON", n: "mercurio_config_json", t: "textarea", mono: true, rows: 4, store: "skin_mercurio_configs.config_json", hint: "Invalid JSON is ignored; blank keeps the previous value." },
  { l: "Mercurio Callback URL", n: "mercurio_callback_url", t: "text", store: "skin_mercurio_configs.callback_url", hint: "Optional override." },

  { l: "Slotomatica Operator ID", n: "st_operatorID", t: "text", store: "skins.st_operatorID" },
  { l: "Slotomatica Entity ID", n: "st_entityID", t: "text", store: "skins.st_entityID" },
  { l: "Slotomatica Api URL", n: "st_api_url", t: "text", store: "skins.st_api_url" },
  { l: "Slotomatica Secret Key", n: "st_secretkey", t: "text", store: "skins.st_secretkey" },
  { l: "Sportsbook", n: "sport_provider", t: "select", store: "skins.sport_provider",
    o: [["sportsbook", "Novusbet"], ["igpixel", "IGPixel"], ["cmswager", "CmsWager"], ["mondogaming", "MondoGaming"]] },
  { l: "Novusbet Endpoint URL", n: "novus_api_url", t: "text", store: "skins.novus_api_url" },
  { l: "Novusbet Client ID", n: "novus_client_id", t: "text", store: "skins.novus_client_id" },
  { l: "Novusbet Secret Key", n: "novus_secret_key", t: "text", store: "skins.novus_secret_key" },
  { l: "Novusbet Prefix", n: "novus_prefix", t: "text", store: "skins.novus_prefix" },
  { l: "IGPixel API URL", n: "igpixel_api_url", t: "text", store: "skins.igpixel_api_url" },
  { l: "IGPixel Username", n: "igpixel_username", t: "text", store: "skins.igpixel_username" },
  { l: "IGPixel Password", n: "igpixel_password", t: "text", store: "skins.igpixel_password" },
  { l: "IGPixel Hash", n: "igpixel_hash", t: "text", store: "skins.igpixel_hash" },
  { l: "CmsWager Username", n: "cmswager_username", t: "text", store: "skins.cmswager_username" },
  { l: "CmsWager API Url", n: "cmswager_api_url", t: "text", store: "skins.cmswager_api_url" },
  { l: "CmsWager Password", n: "cmswager_password", t: "text", store: "skins.cmswager_password" },
  { l: "CmsWager Public Key", n: "cmswager_pub_key", t: "text", store: "skins.cmswager_pub_key" },
  { l: "CmsWager Private Key", n: "cmswager_priv_key", t: "text", store: "skins.cmswager_priv_key" },
  { l: "CmsWager Client Key", n: "cmswager_client_key", t: "text", store: "skins.cmswager_client_key" },
  { l: "MondoGaming API Url", n: "mondogaming_api_url", t: "text", store: "skins.mondogaming_api_url" },
  { l: "MondoGaming Iframe URL", n: "mondogaming_iframe_url", t: "text", store: "skins.mondogaming_iframe_url" },
  { l: "MondoGaming Token", n: "mondogaming_token", t: "text", store: "skins.mondogaming_token" },
  { l: "SHOP Online", n: "online_shop_id", t: "select", store: "skins.online_shop_id", o: "shops" },
  { l: "Meta Title", n: "meta_title", t: "text", store: "skins.meta_title" },
  /* KNOWN BUG (implemented as intended): the Blade names this textarea `meta_title`
     (settings.blade.php:827-828), so it overwrites the title above and nulls
     skins.meta_description on every save. Named correctly here. */
  { l: "Meta description", n: "meta_description", t: "textarea", rows: 3, store: "skins.meta_description", fixed: "named meta_title on the real screen — see the file header" },
  { l: "Footer text", n: "footer_text", t: "textarea", rows: 3, store: "skins.footer_text" },
  { l: "Tawk Chat", n: "tawk_chat_code", t: "textarea", rows: 3, store: "skins.tawk_chat_code" },
  { l: "Info email", n: "info_email", t: "text", store: "skins.info_email" },
  { l: "Documents Email", n: "documents_email", t: "text", store: "skins.documents_email" },
  { l: "Regulation [IT]", n: "regolamento_licenza_it", t: "textarea", rows: 3, store: "skins.regolamento_licenza_it" },
  { l: "Regulation [EN]", n: "regolamento_licenza_en", t: "textarea", rows: 3, store: "skins.regolamento_licenza_en" },

  { crypto: true },   // expands to one row per currencies_cryptoio coin

  { l: "Custom Master Name", n: "custom_settings[custom_master_name]", t: "text", store: "skins.custom_settings" },
  { l: "Custom Agent Name", n: "custom_settings[custom_agent_name]", t: "text", store: "skins.custom_settings" },
  { l: "Custom Promoter Name", n: "custom_settings[custom_promoter_name]", t: "text", store: "skins.custom_settings" },
  { l: "Custom Shop Name", n: "custom_settings[custom_shop_name]", t: "text", store: "skins.custom_settings" },
  { l: "Frontend Homepage Widget Casino Subcategories (separated by ,)", n: "custom_settings[frontend_hp_casino_subcategories]", t: "text", store: "skins.custom_settings" },
  { l: "Frontend Homepage Widget Casino Live Subcategories (separated by ,)", n: "custom_settings[frontend_hp_casinolive_subcategories]", t: "text", store: "skins.custom_settings" },
  { l: "Frontend Crashgames IDs (separated by ,)", n: "custom_settings[frontend_crashgames]", t: "text", store: "skins.custom_settings" },
  { l: "Sportsbook Balance Limit %", n: "custom_settings[sportsbook_balance_limit_perc]", t: "text", store: "skins.custom_settings", hint: "Drives the Limits tab's sport balance calculator." },
  { l: "Sportsbook Balance Limit Currency", n: "custom_settings[sportsbook_balance_limit_currency]", t: "select", store: "skins.custom_settings", o: "currencies" },
  { l: "WhatsApp", n: "custom_settings[whatsapp]", t: "text", store: "skins.custom_settings" },
  { l: "Telegram", n: "custom_settings[telegram]", t: "text", store: "skins.custom_settings" },
  { l: "Reports Multiplier", n: "reports_multiplier", t: "text", store: "skins.reports_multiplier", hint: "The only validated field on this tab — must be 1 or greater." },

  { h: "Softbetexchange" },
  { l: "Endpoint URL", n: "providers[softbetexchange][endpoint]", t: "text", store: "skins_providers_credentials" },
  { l: "API Key", n: "providers[softbetexchange][api_key]", t: "text", store: "skins_providers_credentials" },
  { l: "SDK Url", n: "providers[softbetexchange][sdk_url]", t: "text", store: "skins_providers_credentials" },

  { h: "Savagetech" },
  { l: "Environment", n: "providers[savagetech][environment]", t: "text", store: "skins_providers_credentials" },
  { l: "Vendor ID", n: "providers[savagetech][vendor_id]", t: "text", store: "skins_providers_credentials" },
  { l: "Vendor Secret", n: "providers[savagetech][vendor_secret]", t: "text", store: "skins_providers_credentials" },
  { l: "JWT Access Token Secret", n: "providers[savagetech][jwt_access_token]", t: "text", store: "skins_providers_credentials" },

  { l: "Tawk.to", n: "tawkto_id", t: "text", ph: "https://embed.tawk.to/chatId/widgetId", store: "skins.tawkto_id" },
];

/* ══════════════════════════════════════════════════════════════════════════════════════════════
   4. Mock skins — `skins` rows. Names/codes/currencies stay aligned with window.MOCK.BRANDS (the
      same records the Payments brand switcher offers) so the two surfaces never disagree; ids and
      the remaining columns are drawn deterministically. Ids 69-72 are pinned because the real
      Homepage View controller hard-aliases skins 70/71/72 to skin 69 (SC:4460-4462) — part 2's
      Homepage View tab needs those ids to exist to show that honestly.
   ══════════════════════════════════════════════════════════════════════════════════════════ */
/* Nine skins used to be declared here with hand-picked ids, currencies and
   countries, and every feature flag was decided by `rnd() < 0.16` — a per-skin
   PRNG choosing whether sport was enabled, whether the CMS was on, whether
   deposits were allowed. On a screen whose whole job is "what is turned on for
   this brand", that is the worst possible thing to guess at.

   `skin_settings` is presence-based upstream and here: a setting is ON when a
   row exists for (skin_id, setting). The `value` column is not consulted by the
   enablement check, which is why the mapper below tests for the row rather than
   reading a boolean out of it. */
const hskSkinRow = (r) => {
  const settingRows = Array.isArray(r.skin_settings) ? r.skin_settings : [];
  const settings = {};
  const values = {};
  settingRows.forEach(row => {
    settings[row.setting] = true;          // presence IS the flag
    if (row.value != null && row.value !== "") values[row.setting] = String(row.value);
  });
  const branding = Array.isArray(r.skin_branding) ? (r.skin_branding[0] || {}) : (r.skin_branding || {});
  return {
    id: Number(r.id),
    name: String(r.name || ""),
    skin_code: r.code || "",
    currency: r.currency || "",
    language: r.locale || "",
    timezone: r.timezone || "UTC",
    status: r.status || "active",
    reportsMultiplier: r.reports_multiplier == null ? null : String(r.reports_multiplier),
    /* `country` and `jackpot` were in the seed and are in no column. Country is
       not modelled at all; jackpot enablement is a skin_settings row like every
       other flag, so it is read from there rather than restated. */
    country: null,
    jackpot: !!settings.enable_jackpot,
    domains: Array.isArray(r.skin_domains) ? r.skin_domains : [],
    branding,
    settings,
    values,
    registration: null,   // hydrated on first open of the registration editor
  };
};

const HSK_PAGE_SIZES = [5, 10, 25, 50];   // ajax.js lengthMenu [5,10,25,50], pageLength 50

/* ══════════════════════════════════════════════════════════════════════════════════════════════
   5. Shared tab-body kit. These are the pieces part 2's renderers should build with so every tab
      keeps one rhythm — they are ordinary top-level globals, usable by name from a later file.
   ══════════════════════════════════════════════════════════════════════════════════════════ */

/* Grouped block inside a tab body. */
const HskSection = ({ title, desc, actions, children }) => (
  <div className="hsk-sec">
    {(title || actions) && (
      <div className="hsk-sec__head">
        <div>
          {title && <div className="hsk-sec__title">{title}</div>}
          {desc && <div className="hsk-sec__sub">{desc}</div>}
        </div>
        {actions && <div className="hsk-sec__acts">{actions}</div>}
      </div>
    )}
    {children}
  </div>
);

/* Label ⇄ control row. `name` renders the real input name for traceability. */
const HskField = ({ label, name, hint, error, required, full, children }) => (
  <div className={`hsk-field${full ? " hsk-field--full" : ""}${error ? " hsk-field--err" : ""}`}>
    <label className="hsk-field__l">
      {label}{required && <span className="hsk-req" title="Required">*</span>}
      {name && <code className="hsk-name">{name}</code>}
    </label>
    <div className="hsk-field__c">
      {children}
      {error ? <div className="hsk-field__err"><Icon name="alert" size={11} /> {error}</div>
        : hint ? <div className="hsk-field__h">{hint}</div> : null}
    </div>
  </div>
);

const HskInput = (props) => <input className="input input--sm hsk-input" {...props} />;

const HskTextarea = ({ mono, rows = 3, ...props }) => (
  <textarea className={`input input--sm hsk-input hsk-textarea${mono ? " hsk-mono" : ""}`} rows={rows} {...props} />
);

/* options: ["a","b"] | [[value,label]] | [{value,label}] */
const HskSelect = ({ value, onChange, options = [], ...rest }) => (
  <select className="select input--sm hsk-input" value={value ?? ""} onChange={e => onChange && onChange(e.target.value)} {...rest}>
    {options.map(o => {
      const v = Array.isArray(o) ? o[0] : (o && typeof o === "object" ? o.value : o);
      const l = Array.isArray(o) ? o[1] : (o && typeof o === "object" ? o.label : o);
      return <option key={String(v)} value={v}>{l}</option>;
    })}
  </select>
);

const HskSwitch = ({ value, onChange, on = "On", off = "Off", disabled }) => (
  <Toggle value={!!value} onChange={onChange} onLabel={on} offLabel={off} size="sm" disabled={disabled} />
);

/* Inline callout. tone ∈ info | warn | bug | ok */
const HskNote = ({ tone = "info", icon, title, children }) => (
  <div className={`hsk-note hsk-note--${tone}`}>
    <Icon name={icon || (tone === "warn" || tone === "bug" ? "alert" : tone === "ok" ? "check" : "info")} size={13} />
    <div>{title && <b>{title}</b>}{title && children ? " " : null}{children}</div>
  </div>
);

/* The per-tab save row. The real platform saves one tab at a time through
   POST /skins/saveSkin/{id}/{tab}, so there is deliberately no page-level Save. */
const HskSaveBar = ({ onSave, label = "Save", note, disabled, extra }) => (
  <div className="hsk-savebar">
    <button className="hrs-btn hrs-btn--filters hsk-save" onClick={onSave} disabled={disabled}>
      <Icon name="check" size={14} /> {label}
    </button>
    {extra}
    {note && <span className="hsk-savebar__note">{note}</span>}
  </div>
);

/* Header shown at the top of every tab body: what the tab is, the real route it stands for and the
   real permission gate. Keeps the whole editor traceable without a code dive. */
const HskTabHead = ({ tab, children }) => (
  <div className="hsk-tabhead">
    <div className="hsk-tabhead__row">
      <div className="hsk-tabhead__title">{tab.label}</div>
      {tab.scoped && <span className="hsk-chip hsk-chip--scoped" title="Visible to a scoped Customer Care manager">scoped-CC visible</span>}
    </div>
    {tab.sub && <div className="hsk-tabhead__sub">{tab.sub}</div>}
    <div className="hsk-trace">
      <span><code>{tab.route}</code></span>
      {tab.save && <span>save <code>{tab.save}</code></span>}
      {tab.gate && <span>gate <code>{tab.gate}</code></span>}
    </div>
    {children}
  </div>
);

/* Fallback body for every tab id with no registered renderer. */
const HskTabPending = ({ tab }) => (
  <>
    <HskTabHead tab={tab} />
    <div className="hsk-pending">
      <div className="hsk-pending__mark"><Icon name="sliders" size={18} /></div>
      <div>
        <div className="hsk-pending__t">{tab.label} — not built yet</div>
        <div className="hsk-pending__d">
          This tab is part of the real skin editor (<code>{tab.route}</code>) and is <b>pending</b>.
          {tab.part === 2
            ? <> Its body is delivered by the second half of the §8.2 consolidation and registers itself as <code>HskTabRegistry["{tab.id}"]</code>.</>
            : <> No agent owns this body yet — it still needs building from the reference before the consolidation is complete.</>}
        </div>
        <div className="hsk-pending__d">Nothing is stubbed in its place: an empty tab is honest, a fake form is not.</div>
      </div>
    </div>
  </>
);

/* ══════════════════════════════════════════════════════════════════════════════════════════════
   6. Part-1 tab bodies — Home, Settings (+ the Registration editor), Customer.io pointer.
   ══════════════════════════════════════════════════════════════════════════════════════════ */

/* ---- Home ------------------------------------------------------------------------------------
   skin.blade.php, form skinHome_form → POST /skins/saveSkin/{id}/home (saveEditSkin case "home",
   SC:2811-2865). Six required fields; the Jackpot switch is NOT part of the form — it fires
   GET /skins/setupjackpots/{id}/ immediately (SC:4005-4044, super admin only). */
const HskHomeTab = ({ tab, draft, set, onPatchSkin }) => {
  const [err, setErr] = hskUseState({});

  const save = () => {
    /* Same six required fields as saveNewSkin / saveEditSkin case "home". Message text is written
       for operators (label inferred) — the real ones are backend.insert_name / .insert_code /
       .select_currency / .select_language / .select_timezone / .insert_country, none of which
       resolve to anything in this branch (runtime storage/lang is gitignored). */
    const e = {};
    if (!String(draft.name || "").trim()) e.name = "Enter a name.";
    if (!String(draft.skin_code || "").trim()) e.skin_code = "Enter a code.";   // real platform keys this to errors["name"] — see header SUGGESTION
    if (!draft.currency) e.currency = "Select a currency.";
    if (!draft.language) e.language = "Select a language.";
    if (!draft.timezone) e.timezone = "Select a timezone.";
    if (!draft.country) e.country = "Enter a country.";
    setErr(e);
    if (Object.keys(e).length) { hskToast("Skin not saved", "Fix the highlighted fields and save again."); return; }
    hskToast(`Skin "${draft.name}" saved`, "Skin::update on the six home columns + updatedByUser, then flushSkinCache(id) — which also forgets the CORS origin allowlist and the skins_list cache.");
  };

  const flipJackpot = () => {
    if (draft.jackpot) {
      /* Swal confirm from skin.blade.php:144-196. */
      if (!window.confirm("Are you sure you want to clear the jackpots?")) return;
      set("jackpot", false);
      hskToast("Jackpots cleared", "setupJackpots deleted every jackpots row for this skin and set skins.jackpot = 0. The Jackpot tab now renders its empty state.");
    } else {
      set("jackpot", true);
      hskToast("Jackpots created", "setupJackpots created 3 pots (jp_id 1-3) seeded from the global jp{N}_percentage / _start_balance / _max_range / _min_bet settings, and set skins.jackpot = 1.");
    }
  };

  return (
    <>
      <HskTabHead tab={tab} />
      <HskSection title="Identity & locale" desc="The six columns that define the skin. All six are required; the code must be unique across skins.">
        <HskField label="Name" name="name" required error={err.name}>
          <HskInput value={draft.name || ""} onChange={e => set("name", e.target.value)} />
        </HskField>
        <HskField label="Code" name="skin_code" required error={err.skin_code}
          hint="Unique across all skins — saveNewSkin rejects a duplicate with “code is already in db”.">
          <HskInput value={draft.skin_code || ""} onChange={e => set("skin_code", e.target.value)} />
        </HskField>
        <HskField label="Currency" name="currency" required error={err.currency} hint="currencies() — the currencies_list table.">
          <HskSelect value={draft.currency} onChange={v => set("currency", v)} options={[["", "- Select -"]].concat(HSK_CURRENCIES.map(c => [c, c]))} />
        </HskField>
        <HskField label="Language" name="language" required error={err.language} hint="LanguagesController::getLanguages() — the languages table.">
          <HskSelect value={draft.language} onChange={v => set("language", v)} options={[["", "- Select -"]].concat(HSK_LANGUAGES.map(l => [l, l]))} />
        </HskField>
        <HskField label="Timezone" name="timezone" required error={err.timezone}>
          <HskSelect value={draft.timezone} onChange={v => set("timezone", v)} options={[["", "- Select -"]].concat(HSK_TIMEZONES.map(t => [t, t]))} />
        </HskField>
        <HskField label="Country" name="country" required error={err.country} hint="countries() — ISO code stored on the skin row.">
          <HskSelect value={draft.country} onChange={v => set("country", v)} options={[["", "- Select -"]].concat(HSK_COUNTRIES.map(([iso, n]) => [iso, `${n} (${iso})`]))} />
        </HskField>
      </HskSection>

      <HskSection title="Jackpot" desc="A master switch, not a form field — it takes effect the moment you flip it.">
        <HskField label="Jackpot" name="jackpot"
          hint="Turning it on creates the three pots seeded from the global jp{N}_* settings; turning it off deletes every pot for this skin. Configure amounts on the Jackpot tab.">
          <HskSwitch value={!!draft.jackpot} onChange={flipJackpot} on="Enabled" off="Disabled" />
        </HskField>
        <HskNote tone="warn" title="Super admin only.">
          <code>setupJackpots</code> hard-stops (<code>die</code>) for anyone whose user_level is not 0, even though the switch renders for every admin who can open this tab. The switch is outside the form, so it does not wait for Save — and Save does not undo it.
        </HskNote>
      </HskSection>

      <HskSaveBar onSave={save} note="POST /skins/saveSkin/{id}/home · Skin::update + updatedByUser + flushSkinCache(id)" />
    </>
  );
};

/* ---- Registration editor ---------------------------------------------------------------------
   admin/skins/forms/registrationForm.blade.php + POST /skins/{id}/settings/registration →
   editSkinRegistrationSettings (SC:1050-1237). It is a MODAL on the real screen, not a tab — its
   only trigger is the commented-out "User Registration Form" row at the top of the Settings table,
   which is why it opens from there here too. */
const HSK_REG_DEFAULTS = {
  type: "traditional", step2: true, cpf: false,
  username: true, email: true, phone: false, socials: false,
  help_buttons: [], help_email: "", help_phone: "", min_age: 18,
  reg_free: false,
  validator: "0", validator_registration: false, validator_recovery: false,
  countries: ["AR"], default_country: "AR", shop_country: [],
};

const HskRegistrationModal = ({ skin, value, onClose, onSave }) => {
  /* Two steps (owner ask, 2026-08-16): step 1 is the form itself — the live
     builder plus CPF — and step 2 is everything around the form: help button,
     validator, countries. The split mirrors what the
     two halves ARE: step 1 decides what a player is asked, step 2 decides the
     machinery wrapped around the asking. */
  const [step, setStep] = hskUseState(1);
  const [f, setF] = hskUseState(() => {
    const v = { ...HSK_REG_DEFAULTS, ...(value || {}) };
    /* The help model changed from one mandatory radio to zero-or-more
       checkboxes (owner, 2026-08-16). An old draft carrying `help` converts
       once, keeping what it can: the email button's address; phone had only a
       free-text help_text, which maps to the phone number as well as anything. */
    if (value && value.help && !value.help_buttons) {
      v.help_buttons = [value.help];
      if (value.help === "email") v.help_email = value.help_contact || "";
      if (value.help === "phone") v.help_phone = value.help_text || "";
    }
    return v;
  });
  const [err, setErr] = hskUseState({});
  const [q, setQ] = hskUseState("");
  const [row, setRow] = hskUseState({ shop: "", iso: "" });
  const set = (k, v) => setF(s => ({ ...s, [k]: v }));

  const fast = f.type === "fast";
  const countryList = HSK_COUNTRIES.filter(([iso, n]) => !q.trim() || `${n} ${iso}`.toLowerCase().includes(q.trim().toLowerCase()));

  const toggleCountry = (iso) => {
    const next = f.countries.includes(iso) ? f.countries.filter(c => c !== iso) : f.countries.concat(iso);
    setF(s => ({ ...s, countries: next, default_country: next.includes(s.default_country) ? s.default_country : (next[0] || "") }));
  };

  const submit = () => {
    /* Validation mirrors SC:1060-1120 one rule at a time. */
    const e = {};
    /* The type/methods rules that used to sit here (SC:1060-1120's type check,
       SC:1072's "email alone does not count", the validator-requires-Phone and
       confirm-requires-Email couplings) moved with their subjects: method
       coherence is enforced by save_registration_config in the DATABASE, where
       the form cannot route around it. What validates here is only what this
       modal still edits. */
    if (f.validator !== "0" && !String(f.validator).trim()) e.validator = "Phone verification is on — choose the provider that sends the OTP.";
    if (f.validator !== "0" && !f.validator_registration && !f.validator_recovery) e.validator_scope = "Choose at least one — registration, password recovery, or both.";
    if (!f.countries.length) e.countries = "Select at least one authorized country.";
    if (!f.default_country) e.default_country = "Choose a default country.";
    /* Help is optional now — selecting nothing is a valid answer. What is
       validated is only what a selected button needs. */
    if ((f.help_buttons || []).includes("email")) {
      if (!String(f.help_email).trim()) e.help_email = "The Email button needs an address to write to.";
      else if (!/^\S+@\S+\.\S+$/.test(f.help_email)) e.help_email = "Enter a valid email address.";
    }
    if ((f.help_buttons || []).includes("phone") && !String(f.help_phone).trim()) {
      e.help_phone = "The Phone button needs a number to offer.";
    }
    if (f.min_age === "" || f.min_age == null) e.min_age = "Minimum age is required.";
    setErr(e);
    if (Object.keys(e).length) { hskToast("Registration settings not saved", "Fix the highlighted fields."); return; }
    onSave(f);
  };

  return (
    <div className="bp-modal-scrim hsk-scrim" onClick={onClose}>
      <div className="bp-modal hsk-modal hsk-modal--wide" onClick={e => e.stopPropagation()}>
        <div className="hsk-modal__head">
          <div>
            <div className="hsk-modal__title">User Registration Form</div>
            <div className="hsk-modal__sub">{skin.name} · POST /skins/{skin.id}/settings/registration</div>
          </div>
          <button className="hsk-x" title="Close" onClick={onClose}><Icon name="x" size={14} /></button>
        </div>

        <div className="hsk-modal__body">
          <HskNote tone="bug" title="Half of this screen does not exist on the real platform.">
            Its trigger row is commented out (<code>settings.blade.php:23-35</code>) and the GET half,
            <code>/skins/{"{id}"}/settings/registration</code>, points at <code>SkinsController@showSkinRegistrationSettings</code>
            — a method that is not defined anywhere in this branch — so the form can never load. Only the POST survives, and it still writes
            <code>skin_settings</code>. Rebuilt here as intended rather than left dead; nothing was invented beyond what the surviving form and POST handler define.
          </HskNote>

          {/* TYPE, METHODS AND THE FIELD SET MOVED to the Registration form
              builder at the top of this tab (071, owner ask 2026-08-16). This
              modal used to carry its own fast/traditional radio and method
              checkboxes in prototype state while the builder wrote
              skin_registration_configs — two editors of one decision, one of
              them lying. What stays here is everything the builder does NOT
              own: help button, mobile validator, authorized countries.
              (CPF, Brevo and shop ⇄ country were removed by the owner,
              2026-08-16 — not integrated / not wanted in this surface.) */}
          {/* THE BUILDER, inside the modal — the owner's third and final answer
              on where this lives (it was a tab, then a top-of-tab section, now
              the one place anyone would look: behind the Edit button that says
              "User Registration Form"). Everything registration is in this
              modal; nothing about it renders anywhere else. */}
          <HskSection title="Type, methods and fields"
            desc="Live — saves through its own button below the preview. The Save at the modal's foot covers the sections that follow (help button, validator, countries); the preview reflects them as you edit.">
            {window.Hsk2RegistrationTab
              ? <Hsk2RegistrationTab skin={skin}
                  helpButtons={(f.help_buttons || []).map(t => ({
                    type: t,
                    value: t === "email" ? f.help_email : t === "phone" ? f.help_phone : null }))}
                  otpReg={f.validator !== "0" && !!f.validator_registration}
                  otpRec={f.validator !== "0" && !!f.validator_recovery}
                  allowedCountries={f.countries} defaultCountry={f.default_country} />
              : <div className="hrs-skel" style={{ height: 120 }} />}
          </HskSection>

          {/* OPTIONAL AND MULTIPLE (owner, 2026-08-16): none is fine, several is
              fine, and each selected button asks for exactly the one thing it
              needs — chat needs nothing, email needs an address, phone needs a
              number. The old shape was one mandatory radio plus two shared
              inputs whose enabled/disabled state the operator had to decode. */}
          <HskSection title="Help on registration"
            desc="Optional. Pick none, one or several — each selected button shows on the signup form, and asks only for what it needs.">
            <HskField label="Buttons" name="help-buttons">
              <div className="hsk-checks">
                {[["chat", "Support Chat"], ["email", "Email"], ["phone", "Phone"]].map(([v, l]) => (
                  <label key={v} className="hsk-check">
                    <input type="checkbox" checked={(f.help_buttons || []).includes(v)}
                      onChange={() => setF(s2 => {
                        const cur = s2.help_buttons || [];
                        return { ...s2, help_buttons: cur.includes(v) ? cur.filter(x => x !== v) : cur.concat(v) };
                      })} /> {l}
                  </label>
                ))}
              </div>
            </HskField>
            {(f.help_buttons || []).includes("chat") && (
              <HskField label="Support Chat" name="help-chat"
                hint="No input needed — the button opens the site's support chat.">
                <span style={{ fontSize: 12.5, color: "var(--n-600)" }}>Ready.</span>
              </HskField>
            )}
            {(f.help_buttons || []).includes("email") && (
              <HskField label="Email address" name="help_email" required error={err.help_email}
                hint="Where the Email button writes to. Must be a valid address.">
                <HskInput value={f.help_email} onChange={e => set("help_email", e.target.value)} />
              </HskField>
            )}
            {(f.help_buttons || []).includes("phone") && (
              <HskField label="Phone number" name="help_phone" required error={err.help_phone}
                hint="The number the Phone button offers to call.">
                <HskInput value={f.help_phone} onChange={e => set("help_phone", e.target.value)} />
              </HskField>
            )}
            <HskField label="Minimum age" name="registration_min_age" required error={err.min_age}>
              <HskInput type="number" min={18} max={25} value={f.min_age} onChange={e => set("min_age", e.target.value)} style={{ maxWidth: 120 }} />
            </HskField>
          </HskSection>

          {/* ONE DECISION, then one choice (owner, 2026-08-16). The old shape
              was a provider dropdown plus two independent notification toggles
              — four states that could contradict each other (a provider with
              both toggles off, toggles on with no provider). The redesign:
              verify by OTP, on or off. ON means an OTP at registration AND at
              password recovery — the owner's rule, "both" — so the toggles are
              gone; the only remaining question is which provider sends it.
              OFF clears the provider, so no validator can be selected-but-idle. */}
          <HskSection title="Phone verification (OTP)">
            <HskField label="Verify phone by OTP" name="mobile_validator_enabled"
              hint="On: an OTP verifies the player's number — you choose below whether at registration, at password recovery, or both. Off: no validator is configured at all.">
              <HskSwitch value={f.validator !== "0"}
                onChange={v => v
                  ? setF(s2 => ({ ...s2, validator: "", validator_registration: true, validator_recovery: true }))
                  : setF(s2 => ({ ...s2, validator: "0", validator_registration: false, validator_recovery: false }))} />
            </HskField>
            {f.validator !== "0" && (
              <HskField label="Applies to" name="mobile_validator_scope" required error={err.validator_scope}
                hint="At least one — OTP switched on but applying nowhere would be off wearing a switch.">
                <div className="hsk-checks">
                  <label className="hsk-check">
                    <input type="checkbox" checked={!!f.validator_registration}
                      onChange={e => set("validator_registration", e.target.checked)} /> Registration
                  </label>
                  <label className="hsk-check">
                    <input type="checkbox" checked={!!f.validator_recovery}
                      onChange={e => set("validator_recovery", e.target.checked)} /> Password recovery
                  </label>
                </div>
              </HskField>
            )}
            {f.validator !== "0" && (
              <HskField label="Sent through" name="mobile_validator_id" required error={err.validator}
                hint="The provider that delivers the OTP. Changing it deletes the old validator's config rows and seeds empty-string rows for the new one's config keys.">
                <HskSelect value={f.validator} onChange={v => set("validator", v)}
                  options={[["", "- Select a provider -"]].concat(HSK_VALIDATORS)} />
              </HskField>
            )}
          </HskSection>

          <HskSection title="Authorized countries" desc="Saved by detaching every country from the skin and re-attaching the selection; the default country is flagged is_default on the pivot.">
            <div className="hsk-countrybar">
              <div className="hsk-search">
                <Icon name="search" size={13} />
                <input value={q} onChange={e => setQ(e.target.value)} placeholder="Search country" />
              </div>
              <span className="hsk-countbadge">{f.countries.length} selected</span>
            </div>
            {err.countries && <div className="hsk-field__err"><Icon name="alert" size={11} /> {err.countries}</div>}
            <div className="hsk-countries">
              {countryList.map(([iso, n]) => (
                <label key={iso} className={`hsk-country${f.countries.includes(iso) ? " is-on" : ""}`}>
                  <input type="checkbox" checked={f.countries.includes(iso)} onChange={() => toggleCountry(iso)} />
                  <span>{n}</span><code>{iso}</code>
                </label>
              ))}
            </div>
            <HskField label="Default country" name="default-country" required error={err.default_country}>
              <HskSelect value={f.default_country} onChange={v => set("default_country", v)}
                options={f.countries.length ? f.countries.map(iso => [iso, (HSK_COUNTRIES.find(c => c[0] === iso) || [iso, iso])[1]]) : [["", "- Select a country first -"]]} />
            </HskField>
          </HskSection>
        </div>

        <div className="hsk-modal__foot">
          <button className="btn btn--secondary btn--sm" onClick={onClose}>Cancel</button>
          <button className="hrs-btn hrs-btn--filters hsk-save" onClick={submit}><Icon name="check" size={14} /> Save registration settings</button>
        </div>
      </div>
    </div>
  );
};

/* ---- Settings ---------------------------------------------------------------------------------
   settings.blade.php — ONE Setting / Value table, Save below it. Rendered here in the platform's
   own order: 40 skinSett() toggles → 6 GamesPermissions() toggles → the value rows.
   NOTE: this is NOT the protected 8-toggle "Operational settings" panel from Skins.jsx (brief
   §4.4) — that surface has no counterpart in SkinsController and is neither copied nor
   reimplemented here. See HskProtectedSettingsTab. */
const HskSettingsTab = ({ tab, skin, draft, set }) => {
  const [reg, setReg] = hskUseState(false);
  const [mulErr, setMulErr] = hskUseState("");
  const settings = draft.settings || {};
  const values = draft.values || {};
  const setSett = (k, v) => set("settings", { ...settings, [k]: v });
  const setVal = (n, v) => set("values", { ...values, [n]: v });

  const optionsFor = (o) => o === "promos" ? [["0", "- Select -"]].concat(HSK_PROMOS)
    : o === "shops" ? [["0", "- Select -"]].concat(HSK_SHOPS)
      : o === "currencies" ? [["", "- Select -"]].concat(HSK_CURRENCIES.map(c => [c, c]))
        : o;

  /* The crypto marker expands to one row per currencies_cryptoio coin. */
  const rows = hskUseMemo(() => HSK_VALUE_ROWS.reduce((acc, r) => {
    if (r.crypto) {
      HSK_CRYPTOIO_COINS.forEach(c => acc.push({ l: `${c} Address [CRYPTOIO]`, n: `cryptoio[${c}]`, t: "text", store: "skins.cryptoio_addresses (serialized)", tool: true }));
      return acc;
    }
    acc.push(r); return acc;
  }, []), []);

  const save = () => {
    /* reports_multiplier is the ONE validated field on this tab (SC:3059-3064). */
    const m = values.reports_multiplier;
    if (m !== undefined && m !== "" && (isNaN(Number(m)) || Number(m) < 1)) {
      setMulErr("The reports multiplier cannot be less than 1.");
      hskToast("Settings not saved", "Reports Multiplier must be 1 or greater.");
      return;
    }
    setMulErr("");
    const on = Object.keys(settings).filter(k => settings[k]).length;
    hskToast(`Settings saved for ${skin.name}`,
      `${on} presence rows in skin_settings, the value rows onto skins.*, Cripten/Mercurio upserts, provider credentials into skins_providers_credentials — then flushSkinCache(${skin.id}).`);
  };

  const settingRow = (k, label) => (
    <tr key={k}>
      <th scope="row" className="hsk-kv__k">
        <div className="hsk-kv__kin">
          <span className="hsk-kv__lbl">{label}</span>
          <code className="hsk-name">{`settings[${k}]`}</code>
        </div>
      </th>
      <td className="hsk-kv__v"><HskSwitch value={!!settings[k]} onChange={v => setSett(k, v)} /></td>
    </tr>
  );

  return (
    <>
      <HskTabHead tab={tab} />

      <HskNote tone="info" title="Not the same “Settings” as the prototype's protected panel.">
        This is the real <code>/skins/{"{id}"}/settings/</code> screen. The prototype's 8-toggle
        “Operational settings” panel (registration, KYC-on-signup, age gate, geo-blocking, maintenance
        mode, RTL, dark-default, cookie banner) is protected by brief §4.4, lives in
        <code>src/pages/Skins.jsx</code>, has no counterpart in <code>SkinsController</code>, and is
        neither copied nor reimplemented here — it is served from that file until Phase F.
      </HskNote>

      {/* Row 0 of the real table: the "User Registration Form" edit row. Commented out on the real
          screen; restored here per the known-bug policy — see the modal's own note. */}
      <div className="hsk-regrow">
        <div>
          <div className="hsk-regrow__t">User Registration Form</div>
          <div className="hsk-regrow__d">Registration <b>fields, methods and the username</b> (live), plus: help button, mobile validator, authorized countries.</div>
        </div>
        <button className="btn btn--secondary btn--sm" onClick={() => setReg(true)}><Icon name="edit" size={12} /> Edit</button>
      </div>

      <div className="hsk-settingswrap">
        <table className="hsk-kv">
          <thead><tr><th>Setting</th><th>Value</th></tr></thead>
          <tbody>
            <tr className="hsk-kv__group"><th colSpan={2}>Feature flags — presence rows in <code>skin_settings</code> (a row exists = on)</th></tr>
            {HSK_SKIN_SETT.map(([k, l]) => settingRow(k, l))}

            <tr className="hsk-kv__group"><th colSpan={2}>Game sections — <code>UsersController::GamesPermissions()</code>, same presence storage</th></tr>
            {HSK_GAME_SETT.map(([k, l]) => settingRow(k, l))}

            <tr className="hsk-kv__group"><th colSpan={2}>Values</th></tr>
            {rows.map((r, i) => r.h ? (
              <tr key={`h${i}`} className="hsk-kv__group hsk-kv__group--sub"><th colSpan={2}>{r.h}</th></tr>
            ) : (
              <tr key={r.n}>
                <th scope="row" className="hsk-kv__k">
                  <div className="hsk-kv__kin">
                    <span className="hsk-kv__lbl">
                      {r.l}
                      {r.inferred && <span className="hsk-inferred" title="The real label is a backend.* key that resolves nowhere in this branch">label inferred</span>}
                    </span>
                    <code className="hsk-name">{r.n}</code>
                    {r.store && <span className="hsk-kv__src">{r.store}</span>}
                    {r.fixed && <span className="hsk-kv__bug" title="Known real-platform bug, implemented as intended">{r.fixed}</span>}
                  </div>
                </th>
                <td className="hsk-kv__v">
                  {r.t === "check" ? <HskSwitch value={values[r.n] === "1" || values[r.n] === true} onChange={v => setVal(r.n, v ? "1" : "0")} />
                    : r.t === "select" ? <HskSelect value={values[r.n] ?? ""} onChange={v => setVal(r.n, v)} options={optionsFor(r.o)} />
                      : r.t === "textarea" ? <HskTextarea mono={r.mono} rows={r.rows} value={values[r.n] ?? ""} onChange={e => setVal(r.n, e.target.value)} />
                        : <HskInput value={values[r.n] ?? ""} placeholder={r.ph} onChange={e => setVal(r.n, e.target.value)} />}
                  {r.tool && <a className="hsk-tool" href="/cryptoiotool/" onClick={e => { e.preventDefault(); hskToast("Generate tool", "The real row links to /cryptoiotool/ to derive an address for this coin."); }}>Generate tool</a>}
                  {r.n === "reports_multiplier" && mulErr && <div className="hsk-field__err"><Icon name="alert" size={11} /> {mulErr}</div>}
                  {r.hint && <div className="hsk-field__h">{r.hint}</div>}
                </td>
              </tr>
            ))}
          </tbody>
        </table>
      </div>

      <HskNote tone="warn" title="What this Save does not touch.">
        <code>print_always_copy</code> and <code>enable_sport_jackpots</code> are managed from other tabs and are never unsubscribed here, and neither are the Customer.io-managed keys
        (<code>cio_cdp_enabled</code>, <code>cio_send_players</code>, <code>cio_cdp_trigger_*</code>) — which is why no Customer.io row appears in the table above, even though the legacy prototype put five of them there.
        Unsubscribe also only deletes rows whose value is NULL, so any key the registration editor wrote survives a toggle-off.
      </HskNote>
      <HskNote tone="info" title="Two dead inputs, honestly absent.">
        “Tipologia pagina casino live” (<code>casinolive_page_v</code>) is commented out in both the view and the save path, so it is not rendered here.
        The <code>vsettings[]</code> half of this tab's save is a no-op — the real settings form posts none, those inputs live on the Sport, Financial and Security tabs.
      </HskNote>

      <HskSaveBar onSave={save} label="Save settings" note="POST /skins/saveSkin/{id}/settings" />

      {reg && (
        <HskRegistrationModal skin={skin} value={draft.registration} onClose={() => setReg(false)}
          onSave={(v) => { set("registration", v); setReg(false); hskToast("Registration settings saved", `skin_settings rows written for help_*, registration_min_age and mobile_validator_*; ${v.countries.length} country pivot rows attached with ${v.default_country} as default.`); }} />
      )}
    </>
  );
};

/* ---- Customer.io ------------------------------------------------------------------------------
   The tab is real (template.blade.php:85-90 → /skins/{id}/customerio/, rendered by the catch-all
   showTab with NO role check). The PANEL is not ours: the prototype implements Customer.io / CDP
   configuration inside the protected src/pages/Settings.jsx (brief §4.1). Per §8.2 it stays there —
   this tab points at it instead of copying it. */
const HskCustomerIoTab = ({ tab, skin }) => {
  /* THE REAL PANEL, wired to the real table (070). What was here before was a
     pointer reading "Customer.io configuration lives in PayBO Settings" — wrong
     twice over. The owner decided it lives HERE (2026-08-16, overriding the
     §8.2 reconciliation). And the panel it pointed at did not render:
     CustomerioPanel was defined in Settings.jsx and rendered by NOTHING, so the
     button walked an operator to a page that does not contain the thing it
     promised.

     The component itself still lives in Settings.jsx — every top-level name is
     a window global here, so rendering it from this file is exactly how the
     no-second-copy rule is kept while the surface moves. */
  const [tick, setTick] = React.useState(0);
  const [busy, setBusy] = React.useState(false);
  const [msg, setMsg] = React.useState(null);
  const [err, setErr] = React.useState(null);
  const [cfg, setCfg] = React.useState(null);

  const feed = useHrsFetch(
    () => window.sb.list("skinCustomerioConfigs", { limit: 1, filters: { skin: skin.id } }),
    [skin.id, tick]);

  React.useEffect(() => {
    if (feed.loading || feed.error) return;
    const row = (feed.data || [])[0] || null;
    setCfg(normalizeCustomerioConfig(row ? {
      enabled: row.enabled,
      credentials: { siteId: row.site_id, apiKey: row.api_key, webhookSecret: row.webhook_secret },
      triggers: row.triggers,
    } : null));
  }, [feed.data, feed.loading, feed.error]);

  const save = async () => {
    if (busy || !cfg) return;
    setBusy(true); setErr(null); setMsg(null);
    const r = await window.sb.upsertCustomerioConfig({
      skinId: Number(skin.id), enabled: cfg.enabled,
      siteId: cfg.credentials.siteId, apiKey: cfg.credentials.apiKey,
      webhookSecret: cfg.credentials.webhookSecret, triggers: cfg.triggers,
    });
    setBusy(false);
    if (!r || !r.ok) { setErr((r && r.error && r.error.message) || "Refused."); return; }
    setMsg("Saved."); setTick(t => t + 1);
  };

  return (
    <>
      <HskTabHead tab={tab} />
      {feed.loading && !cfg ? <div className="hrs-skel" style={{ height: 200 }} /> :
       feed.error ? <HrsError error={feed.error} onRetry={() => setTick(t => t + 1)} /> :
       cfg && (
        <div style={{ display: "grid", gap: 12 }}>
          <CustomerioPanel cfg={cfg} setCfg={setCfg} webhookId={skin.code || skin.id} />
          <div style={{ display: "flex", gap: 10, alignItems: "center" }}>
            <button className="rpt-btn rpt-btn--blue" style={{ minWidth: 0, height: 40 }}
              disabled={busy} onClick={save}>
              {busy ? "Saving…" : "Save Customer.io configuration"}
            </button>
            {msg && <span style={{ fontSize: 13, color: "var(--g-700, #15803d)" }}>{msg}</span>}
          </div>
          {err && <div className="hu-errbox"><Icon name="alert" size={14} /> {err}</div>}
          <HskNote tone="info" title="Who can save.">
            <code>upsert_customerio_config</code> admits a super admin or this skin's own
            admin — the same audience isystem's screen has for <code>skins.cio_siteid</code> /
            <code>cio_apikey</code>. The trigger shape is validated in the database.
          </HskNote>
        </div>
      )}
    </>
  );
};

/* ---- The protected Operational-settings pointer -----------------------------------------------
   Registered under its own id, deliberately NOT in HSK_TABS: the real platform has no such tab, and
   inventing one would break the "nothing added" rule. It exists so the orchestrator (or Phase F) can
   surface the protected panel from this shell without anyone re-implementing it here. */
/* Phase F / F12 — §8.2 close-out. The Operational-settings tab is ported here from
   Skins.jsx's protected `renderSettings()` so the consolidated editor carries it and the
   orphaned `brands` route can retire, exactly as brief §8.2 asks ("preserve its protected
   Settings tab exactly" while consolidating).

   Ported verbatim: the same 8 toggle keys in the same order, the same labels, the same
   hints, the same defaults, and the same `settings` draft shape. Only the layout helpers
   differ (this file's HskSection/HskField/HskSwitch instead of Skins.jsx's local Section/
   Field/Toggle), which is what makes it match the rest of this editor visually.

   src/pages/Skins.jsx itself is NOT modified — it stays byte-identical. */
const HSK_OPERATIONAL_TOGGLES = [
  ["registration",  "Open registration",        "Players can sign up directly. Off = invite-only."],
  ["kycOnSignup",   "KYC required on signup",   "Block deposits until verification is complete."],
  ["ageGate",       "Age-gate splash screen",   "Show 18+ confirmation before the homepage loads."],
  ["geoBlocking",   "Geo-blocking enforcement", "Match the player's country against the licence allow-list."],
  ["maintenance",   "Maintenance mode",         "Replaces the homepage with the maintenance template."],
  ["rtl",           "Right-to-left layout",     "Mirror the layout for Arabic / Hebrew users."],
  ["dark",          "Dark theme as default",    "Override the player's local preference."],
  ["cookieBanner",  "Cookie consent banner",    "Required for EEA / UK markets."],
];

const HskProtectedSettingsTab = ({ draft, set }) => {
  const toggles = (draft && draft.settings) || {
    maintenance: false, geoBlocking: true, kycOnSignup: false, ageGate: true,
    cookieBanner: true, rtl: false, dark: true, registration: true,
  };
  const setToggle = (k, v) => set && set("settings", { ...toggles, [k]: v });
  return (
    <HskSection title="Operational settings" desc="Toggles that control how the player-facing site behaves.">
      {HSK_OPERATIONAL_TOGGLES.map(([k, label, hint]) => (
        <HskField key={k} label={label} hint={hint}>
          <HskSwitch value={!!toggles[k]} onChange={v => setToggle(k, v)} />
        </HskField>
      ))}
    </HskSection>
  );
};

/* ══════════════════════════════════════════════════════════════════════════════════════════════
   7. THE TAB REGISTRY — tab id → React component. Looked up at render time, so a later-loading
      file fills the remaining tabs by assigning into this same object:

          HskTabRegistry["graphic"] = Hsk2GraphicTab;

      Renderers are COMPONENTS (they may use hooks) and receive:
        tab          the HSK_TABS descriptor { id, label, sub, slug, icon, route, save, gate, scoped, part }
        skin         the saved skin record (id, name, skin_code, currency, language, timezone, country, jackpot, …)
        skins        every skin in the list — for cross-skin pickers (e.g. "apply settings from another skin")
        draft        the working copy being edited (same shape as skin, plus settings / values / registration)
        set(k, v)    set one key on draft
        patch(obj)   merge an object into draft
        onPatchSkin(obj)  commit straight to the saved record (used by out-of-form switches)
      Anything unregistered falls back to HskTabPending.
   ══════════════════════════════════════════════════════════════════════════════════════════ */
const HskTabRegistry = {
  home: HskHomeTab,
  settings: HskSettingsTab,
  customerio: HskCustomerIoTab,
  "operational-settings": HskProtectedSettingsTab,   // protected pointer — not a real platform tab
};
window.HskTabRegistry = HskTabRegistry;

/* ══════════════════════════════════════════════════════════════════════════════════════════════
   8. Editor shell — back arrow, identity, skin switcher, pill tab strip, tab body.
      Shape ported from Skins.jsx's SkinDetail (pill tabs + per-tab subtitles + grouped forms);
      content and save semantics from template.blade.php.
   ══════════════════════════════════════════════════════════════════════════════════════════ */
const HskEditor = ({ skin, skins, onBack, onPick, onPatchSkin }) => {
  const [tabId, setTabId] = window.useUrlTab("/cms/skins", HSK_TAB_ROUTES, "home");
  const [draft, setDraft] = hskUseState(() => ({ ...skin }));

  /* Switching skins from the header rebuilds the draft. */
  hskUseEffect(() => { setDraft({ ...skin }); }, [skin.id]);

  /* Esc returns to the list — same affordance as Skins.jsx's editor. */
  hskUseEffect(() => {
    const onKey = (e) => { if (e.key === "Escape") onBack(); };
    document.addEventListener("keydown", onKey);
    return () => document.removeEventListener("keydown", onKey);
  }, [onBack]);

  const set = (k, v) => setDraft(d => ({ ...d, [k]: v }));
  const patch = (obj) => setDraft(d => ({ ...d, ...obj }));

  const tab = HSK_TAB_BY_ID[tabId] || HSK_TABS[0];
  const Body = HskTabRegistry[tab.id] || HskTabPending;
  const initials = String(draft.name || "??").slice(0, 2).toUpperCase();

  return (
    <div className="page report-page hrs-page hsk-page">
      <div className="page__header hsk-head">
        <div className="hsk-ident">
          <button className="hsk-back" onClick={onBack} title="Back to Skins (Esc)"><Icon name="chevron_left" size={16} /></button>
          <div className="hsk-mark" aria-hidden="true">{initials}</div>
          <div className="hsk-idtext">
            <div className="page__title hrs-title">
              Edit skin
              <Tip>
                Real-platform access: <code>showSkinDetails</code> aborts <b>404</b> unless <code>isadmin()</code>. A scoped Customer Care manager
                (<code>!isadmin() &amp;&amp; isCustomCare()</code>) sees only the three catalog tabs their <code>support_skin_*</code> permissions allow —
                Provider, Games and Subcategories — and can save only Provider.
              </Tip>
            </div>
            <div className="page__subtitle">
              {draft.name} · <code>{draft.skin_code}</code> · skin #{skin.id} · {draft.currency}
            </div>
          </div>
        </div>

        {/* Prototype navigation aid: the real header has no skin switcher — you go back to /skins/
            and click another name. It moves between the same records and exposes no extra data. */}
        <div className="hsk-picker">
          <label htmlFor="hsk-skinpick">Skin</label>
          <select id="hsk-skinpick" className="select input--sm" value={skin.id} onChange={e => onPick(Number(e.target.value))}>
            {skins.map(s => <option key={s.id} value={s.id}>{s.id} — {s.name}</option>)}
          </select>
        </div>
      </div>

      {/* Tab strip — pills that scroll on tablet; a select replaces them on phones (brief §11). */}
      <div className="hsk-tabsel">
        <label htmlFor="hsk-tabpick">Section</label>
        <select id="hsk-tabpick" className="select input--sm" value={tab.id} onChange={e => setTabId(e.target.value)}>
          {HSK_TABS.map(t => <option key={t.id} value={t.id}>{t.label}{HskTabRegistry[t.id] ? "" : " — pending"}</option>)}
        </select>
      </div>
      <div className="panel hsk-tabsbar">
        <div className="hsk-tabs" role="tablist">
          {HSK_TABS.map(t => {
            const on = t.id === tab.id;
            const ready = !!HskTabRegistry[t.id];
            return (
              <button key={t.id} role="tab" aria-selected={on} title={t.sub}
                className={`hsk-tab${on ? " is-on" : ""}${ready ? "" : " is-pending"}`}
                onClick={() => setTabId(t.id)}>
                <Icon name={t.icon} size={12} /> {t.label}
                {!ready && <span className="hsk-dot" title="Not built yet" />}
              </button>
            );
          })}
        </div>
      </div>

      <div className="panel hsk-body">
        <Body tab={tab} skin={skin} skins={skins} draft={draft} set={set} patch={patch} onPatchSkin={onPatchSkin} />
      </div>
    </div>
  );
};

/* ══════════════════════════════════════════════════════════════════════════════════════════════
   9. List-view modals — New Skin, Delete, CF Purge.
   ══════════════════════════════════════════════════════════════════════════════════════════ */

/* GET /newSkinForm → newSkin.blade.php; POST /saveNewSkin → saveNewSkin (SC:3944-4004). */
const HskNewSkinModal = ({ codes, onClose, onCreate }) => {
  const [f, setF] = hskUseState({ name: "", skin_code: "", currency: "", timezone: "", language: "", country: "" });
  const [err, setErr] = hskUseState({});
  const set = (k, v) => setF(s => ({ ...s, [k]: v }));

  const submit = () => {
    const e = {};
    if (!f.name.trim()) e.name = "Enter a name.";                       /* label inferred (backend.insert_name) */
    if (!f.skin_code.trim()) e.skin_code = "Enter a code.";             /* real platform writes this into errors["name"] — see header SUGGESTION */
    else if (codes.includes(f.skin_code.trim().toLowerCase())) e.skin_code = "This code is already in the database.";
    if (!f.currency) e.currency = "Select a currency.";
    if (!f.language) e.language = "Select a language.";
    if (!f.timezone) e.timezone = "Select a timezone.";
    if (!f.country) e.country = "Select a country.";
    setErr(e);
    if (Object.keys(e).length) return;
    onCreate(f);
  };

  return (
    <div className="bp-modal-scrim hsk-scrim" onClick={onClose}>
      <div className="bp-modal hsk-modal" onClick={e => e.stopPropagation()}>
        <div className="hsk-modal__head">
          <div>
            <div className="hsk-modal__title">New Skin</div>
            <div className="hsk-modal__sub">POST /saveNewSkin · Skin::create + addedByUser, then flushSkinsList()</div>
          </div>
          <button className="hsk-x" title="Close" onClick={onClose}><Icon name="x" size={14} /></button>
        </div>
        <div className="hsk-modal__body">
          <HskField label="Name" name="name" required error={err.name}>
            <HskInput value={f.name} onChange={e => set("name", e.target.value)} />
          </HskField>
          <HskField label="Code" name="skin_code" required error={err.skin_code} hint="Must be unique across skins.">
            <HskInput value={f.skin_code} onChange={e => set("skin_code", e.target.value)} />
          </HskField>
          <HskField label="Currency" name="currency" required error={err.currency}>
            <HskSelect value={f.currency} onChange={v => set("currency", v)} options={[["", "- Select -"]].concat(HSK_CURRENCIES.map(c => [c, c]))} />
          </HskField>
          <HskField label="Timezone" name="timezone" required error={err.timezone}>
            <HskSelect value={f.timezone} onChange={v => set("timezone", v)} options={[["", "- Select -"]].concat(HSK_TIMEZONES.map(t => [t, t]))} />
          </HskField>
          <HskField label="Language" name="language" required error={err.language}>
            <HskSelect value={f.language} onChange={v => set("language", v)} options={[["", "- Select -"]].concat(HSK_LANGUAGES.map(l => [l, l]))} />
          </HskField>
          <HskField label="Country" name="country" required error={err.country}>
            <HskSelect value={f.country} onChange={v => set("country", v)} options={[["", "- Select -"]].concat(HSK_COUNTRIES.map(([iso, n]) => [iso, `${n} (${iso})`]))} />
          </HskField>
        </div>
        <div className="hsk-modal__foot">
          <button className="btn btn--secondary btn--sm" onClick={onClose}>Cancel</button>
          <button className="hrs-btn hrs-btn--filters hsk-save" onClick={submit}><Icon name="check" size={14} /> Create skin</button>
        </div>
      </div>
    </div>
  );
};

/* GET /skins/delete/{id}/ → SC:4153-4164 — a bare Skin::delete with no cascade. */
const HskDeleteDialog = ({ skin, onClose, onConfirm }) => (
  <div className="bp-modal-scrim hsk-scrim" onClick={onClose}>
    <div className="bp-modal hsk-modal hsk-modal--sm" onClick={e => e.stopPropagation()}>
      <div className="hsk-modal__head">
        <div className="hsk-modal__title">Delete skin</div>
        <button className="hsk-x" title="Close" onClick={onClose}><Icon name="x" size={14} /></button>
      </div>
      <div className="hsk-modal__body">
        <p className="hsk-p">Delete <b>{skin.name}</b> (#{skin.id})?</p>
        <HskNote tone="warn" title="Nothing cascades.">
          <code>delete</code> removes the <code>skins</code> row only. Domains, settings, provider links, deposit and withdrawal methods, jackpots and every other per-skin table are left behind as orphans.
        </HskNote>
      </div>
      <div className="hsk-modal__foot">
        <button className="btn btn--secondary btn--sm" onClick={onClose}>Cancel</button>
        <button className="hrs-btn hrs-btn--reset" onClick={onConfirm}><Icon name="trash" size={13} /> Delete skin</button>
      </div>
    </div>
  </div>
);

/* GET /skins/purgeCFCache/{id}/ (routes/admin.php:125-127). */
const HskPurgeDialog = ({ skin, onClose, onConfirm }) => (
  <div className="bp-modal-scrim hsk-scrim" onClick={onClose}>
    <div className="bp-modal hsk-modal hsk-modal--sm" onClick={e => e.stopPropagation()}>
      <div className="hsk-modal__head">
        <div className="hsk-modal__title">Purge Cloudflare cache</div>
        <button className="hsk-x" title="Close" onClick={onClose}><Icon name="x" size={14} /></button>
      </div>
      <div className="hsk-modal__body">
        <p className="hsk-p">Purge the Cloudflare cache for <b>{skin.name}</b> (#{skin.id})?</p>
        <HskNote tone="info">Uses the zone from this skin's <code>CF ZONE ID</code> setting. The list's “CF Purge All Skins” button exists in the Blade but is commented out, so there is no all-skins purge here either.</HskNote>
      </div>
      <div className="hsk-modal__foot">
        <button className="btn btn--secondary btn--sm" onClick={onClose}>Cancel</button>
        <button className="hrs-btn hrs-btn--filters" onClick={onConfirm}><Icon name="refresh" size={13} /> Purge</button>
      </div>
    </div>
  </div>
);

/* ══════════════════════════════════════════════════════════════════════════════════════════════
   10. The page — skin list, and the editor once a skin is opened.
       Shadows the legacy HostSkins.jsx global of the same name.
   ══════════════════════════════════════════════════════════════════════════════════════════ */
const HostSkinsUnified = () => {
  window.useLocale && window.useLocale();

  const feed = useHrsFetch(() => window.sb.list("skins", { limit: 500 }), []);
  const skins = hskUseMemo(() => (feed.data || []).map(hskSkinRow), [feed.data]);
  const save = useHrsSave(feed);
  const [openId, setOpenId] = hskUseState(null);
  const [draftF, setDraftF] = hskUseState({ id: "", name: "" });
  const [applied, setApplied] = hskUseState({ id: "", name: "" });
  const [sort, setSort] = hskUseState({ key: "id", dir: "asc" });   // server default: skins.id ASC
  const [page, setPage] = hskUseState(0);
  const [pageSize, setPageSize] = hskUseState(50);                  // ajax.js pageLength 50
  const [creating, setCreating] = hskUseState(false);
  const [del, setDel] = hskUseState(null);
  const [purge, setPurge] = hskUseState(null);

  const open = skins.find(s => s.id === openId) || null;

  if (open) {
    return (
      <HskEditor
        skin={open} skins={skins}
        onPick={(id) => setOpenId(id)}
        /* Two different tables behind one callback, because the editor's
           switches mix them. `settings` is PRESENCE-based: a flag is on when a
           row exists for (skin_id, setting), so turning one on is an INSERT and
           turning it off is a DELETE — there is no boolean to update. Anything
           else is a column on `skins`. */
        onPatchSkin={(obj) => {
          const settings = obj && obj.settings;
          if (settings && typeof settings === "object") {
            const entries = Object.entries(settings);
            return save.run(async () => {
              for (const [setting, on] of entries) {
                const res = on
                  ? await window.sb.create("skinSettings", { skin_id: open.id, setting, value: "1" })
                  : await window.sb.remove("skinSettings", open.id, { setting: `eq.${setting}` });
                if (!res.ok) return res;
              }
              return { ok: true, data: entries.length, meta: {} };
            }, { done: "Settings saved", fail: "Settings were NOT saved" });
          }
          return save.run(() => window.sb.update("skins", open.id, obj),
            { done: "Skin saved", fail: "Skin was NOT saved" });
        }}
        onBack={() => {
          try { if (window.location.pathname !== "/cms/skins") window.history.pushState(null, "", "/cms/skins"); } catch (_e) {}
          setOpenId(null);
        }} />
    );
  }

  /* Filters: ID is an exact match, Name a LIKE '%…%' (SC:4074-4080). Nothing else exists. */
  const rows = skins.filter(s => {
    if (String(applied.id).trim() && String(s.id) !== String(applied.id).trim()) return false;
    if (String(applied.name).trim() && !s.name.toLowerCase().includes(String(applied.name).trim().toLowerCase())) return false;
    return true;
  });

  /* KNOWN BUG (implemented as intended): getSkinsListTable's order-by switch cases are "id" and
     "username" while the column is registered as `name`, so Name never sorts on the real platform
     and the server falls back to skins.id ASC. Sorting works here. */
  const sorted = rows.slice().sort((a, b) => {
    const d = sort.dir === "asc" ? 1 : -1;
    if (sort.key === "name") return a.name.localeCompare(b.name) * d;
    return (a.id - b.id) * d;
  });
  const safePage = Math.min(page, Math.max(0, Math.ceil(sorted.length / pageSize) - 1));
  const paged = sorted.slice(safePage * pageSize, safePage * pageSize + pageSize);

  const FIELDS = [
    { key: "id", label: "ID", type: "text", icon: "list", placeholder: "Exact id" },
    { key: "name", label: "Name", type: "text", icon: "search", placeholder: "Contains…", grow: true },
  ];

  const actions = (r) => (
    <div className="hsk-acts">
      <button className="hsk-act hsk-act--edit" title="Edit skin" onClick={() => setOpenId(r.id)}><Icon name="edit" size={13} /></button>
      <button className="hsk-act" title="Purge Cloudflare cache" onClick={() => setPurge(r)}><Icon name="refresh" size={13} /></button>
      <button className="hsk-act hsk-act--danger" title="Delete skin" onClick={() => setDel(r)}><Icon name="trash" size={13} /></button>
    </div>
  );

  const columns = [
    { key: "id", label: "ID", align: "left", sortable: true, width: 90, render: r => <span className="hsk-id">{r.id}</span> },
    {
      key: "name", label: "Name", align: "left", sortable: true, render: r => (
        <button className="hsk-namelink" onClick={() => setOpenId(r.id)} title="Open the skin editor">
          <span>{r.name}</span><Icon name="chevron_right" size={13} />
        </button>
      )
    },
    { key: "_acts", label: "Actions", align: "center", width: 140, render: actions },
  ];

  return (
    <HrsShell
      title="Skins"
      subtitle="One record per white-label brand — its locale, its integrations, its player-facing surface"
      gate={<>Real-platform access: <b>Super Admin only</b> — <code>SkinsController::index</code> aborts <code>404</code> unless <code>isadmin()</code>. </>}
      gateNote={<>Honest asymmetry: the DataTable feed <code>GET /getskins/</code> <b>does</b> support non-super-admins, scoping rows to <code>$user-&gt;getSkinIDS()</code> — so the data is reachable for skin-scoped users even though the page they would land on 404s.</>}
      explainer={{
        bullets: [
          <>A <b>skin</b> is one branded front end: its own domain(s), currency, language, provider catalogue, payment methods and legal copy, all hanging off one <code>skins</code> row. Everything an operator can change about a brand is a tab of the editor behind a name in this list.</>,
          <>The list itself is deliberately thin — <b>ID, Name, Actions</b> is the entire real table, filtered by exact ID or a name contains. Player counts, balances, publish state and SSL status are <i>not</i> part of this screen on the real platform, so they are not shown here.</>,
          <>Each tab of the editor saves on its own through <code>POST /skins/saveSkin/{"{id}"}/{"{tab}"}</code>. There is no page-level Save, and most tabs end by flushing the skin's cache family — including the CORS origin allowlist and the global <code>skins_list</code>.</>,
          <><b>Delete does not cascade.</b> It removes the <code>skins</code> row and nothing else, leaving every per-skin table orphaned.</>,
        ]
      }}
      actions={<button className="hrs-btn hrs-btn--filters" onClick={() => setCreating(true)}><Icon name="plus" size={14} /> New Skin</button>}>

      <HrsFilters
        fields={FIELDS} values={draftF}
        onChange={(k, v) => setDraftF(d => ({ ...d, [k]: v }))}
        onSearch={(v) => { setApplied(v); setPage(0); }}
        onReset={() => { setDraftF({ id: "", name: "" }); setApplied({ id: "", name: "" }); setPage(0); }}
        resultLabel={`${window.hrsInt ? window.hrsInt(sorted.length) : sorted.length} of ${skins.length}`} />

      {/* Signed out, RLS returns zero skins — which would render as "this
          platform has no brands". Never true, never actionable. */}
      <HrsAsync state={feed} skeletonRows={6} skeletonCols={3}
                empty="No skins yet — create one with New Skin.">
        {() => (<>
      <HrsTable
        columns={columns} rows={paged} rowKey="id"
        sort={sort} onSort={(s) => { setSort(s); setPage(0); }}
        empty={applied.id || applied.name ? "No skin matches these filters." : "No skins yet — create one with New Skin."}
        renderCard={r => (
          <>
            <div className="hrs-card__top">
              <b>{r.name}</b>
              <span className="hsk-id">ID {r.id}</span>
            </div>
            <div className="hrs-card__grid">
              <span>Code</span><b>{r.skin_code}</b>
              <span>Currency</span><b>{r.currency}</b>
            </div>
            <div className="hsk-card__acts">
              <button className="btn btn--secondary btn--sm" onClick={() => setOpenId(r.id)}><Icon name="edit" size={12} /> Edit</button>
              <button className="btn btn--ghost btn--sm" onClick={() => setPurge(r)}><Icon name="refresh" size={12} /> CF Purge</button>
              <button className="btn btn--ghost btn--sm hsk-card__del" onClick={() => setDel(r)}><Icon name="trash" size={12} /> Delete</button>
            </div>
          </>
        )} />

      <HrsPager page={safePage} pageSize={pageSize} total={sorted.length} sizes={HSK_PAGE_SIZES}
        onPage={setPage} onPageSize={(n) => { setPageSize(n); setPage(0); }} />
        </>)}
      </HrsAsync>

      <div className="hsk-listnote">
        <Icon name="info" size={12} />
        Row actions follow the real screen: <b>Edit</b> for everyone who reaches the page, <b>Delete</b> and <b>CF Purge</b> only when <code>user.is_admin == 1</code>.
        The Blade's “CF Purge All Skins” header button is commented out, so it is absent here too.
      </div>

      {creating && (
        <HskNewSkinModal
          codes={skins.map(s => s.skin_code.toLowerCase())}
          onClose={() => setCreating(false)}
          onCreate={(f) => {
            setCreating(false);
            /* No id: the identity column assigns it. Every feature flag starts
               OFF for free, because skin_settings is presence-based and a new
               skin has no rows there. */
            save.run(() => window.sb.create("skins", {
              code: f.skin_code.trim(), name: f.name.trim(), currency: f.currency,
              locale: f.language, timezone: f.timezone,
            }), { done: `Skin "${f.name.trim()}" created`, fail: "Skin was NOT created" });
          }} />
      )}

      {del && (
        <HskDeleteDialog skin={del} onClose={() => setDel(null)}
          /* Archive, not delete. There is deliberately no DELETE policy on
             `skins` (013): upstream the delete removes the row and nothing
             else, orphaning every per-skin table, and here the foreign keys
             would refuse it anyway. `status = archived` retires a brand
             without breaking everything that points at it. */
          onConfirm={() => {
            setDel(null);
            save.run(() => window.sb.update("skins", del.id, { status: "archived" }),
              { done: `Skin "${del.name}" archived`, fail: `Skin "${del.name}" was NOT archived` });
          }} />
      )}

      {purge && (
        <HskPurgeDialog skin={purge} onClose={() => setPurge(null)}
          onConfirm={() => {
            setPurge(null);
            hskToast("Not purged — no write path yet",
              `Would call GET /skins/purgeCFCache/${purge.id}/, using the CF ZONE ID setting on this skin. That is an outbound call to Cloudflare, not a database write, and it is not wired.`);
          }} />
      )}
    </HrsShell>
  );
};

/* Exported under its own name since Aug 2026. This file used to overwrite the
   legacy `HostSkins` global purely by loading after src/pages/HostSkins.jsx —
   correct only because of <script> order, which meant reordering two tags in
   index.html would silently ship the old screen. HostSkins.jsx is now deleted
   and app.jsx renders <HostSkinsUnified/> directly.

   The `brand` prop the route switch used to pass is gone with it: this screen
   is not brand-scoped, it manages every skin. */
window.HostSkinsUnified = HostSkinsUnified;
