/* global window, React */
// ─────────────────────────────────────────────────────────────────────────
// DEDICATED RESOURCES, single-screen inline flow
// ─────────────────────────────────────────────────────────────────────────
// Layout:
//   1. Time toggle             (Part-Time / Full-Time, segmented)
//   2. Roles section header    (title + Onboarding Availability widget)
//   3. Roles grid              (glass cards, multi-select)
//   4. Inline config panel     (one panel, switches between selected roles via tabs)
//        · PT: day-rate plan tiles (1 / 5 / 10 / Custom) + savings
//        · FT: location / seniority / tasks form
//   5. Deposit summary         (refundable deposit + per-role line items)
//
// Per-role configuration is stored in `selection.roleConfigs[roleId]`:
//   FT: { location, seniority, tasks }
//   PT: { days: number }            // base day-rate plan
// ─────────────────────────────────────────────────────────────────────────

const { useState: dfUseState, useEffect: dfUseEffect } = React;

const DEDICATED_DEPOSIT = 250;
window.DEDICATED_DEPOSIT = DEDICATED_DEPOSIT;

// Day-rate plans, fixed schedule of discounts off the role's base day rate.
// Custom = 11+ days, gets the deepest discount.
const DAY_PLANS = [
  { id: 'd1',     days: 1,  discount: 0,    label: '1 Day',  blurb: 'Ideal for urgent requests or small tasks' },
  { id: 'd5',     days: 5,  discount: 0.10, label: '5 Days', blurb: 'or a total of {total}, giving you {pct}% off' },
  { id: 'd10',    days: 10, discount: 0.12, label: '10 Days',blurb: 'or a total of {total}, giving you {pct}% off' },
  { id: 'custom', days: 11, discount: 0.15, label: 'Custom', blurb: 'Enter 11 days or more for tailored support' },
];

// Per-location range multipliers applied to a role's `hourlyFrom` baseline
// (which is the Philippines floor on each role card). The (lo, hi) pair spans
// junior → senior so a single per-country band is shown to the visitor.
// Final price is reconfirmed post-shortlist; these are indicative only.
const FT_LOCATIONS = [
  // 2026-06-10 (Loom 34): hourlyFrom = the location's Junior ladder rate
  // (fixed seniority ladder × the location's low multiplier), shown on the
  // pill as "from £X/hour".
  { id: 'uk',           label: 'United Kingdom', flag: '🇬🇧', cc: 'gb', multLo: 2.8, multHi: 4.0, hourlyFrom: 21 },
  { id: 'philippines',  label: 'Philippines',    flag: '🇵🇭', cc: 'ph', multLo: 1.0, multHi: 1.5, hourlyFrom: 7.5 },
  { id: 'south-africa', label: 'South Africa',   flag: '🇿🇦', cc: 'za', multLo: 1.4, multHi: 2.1, hourlyFrom: 10.5 },
];

// Compute the indicative hourly range for a given role × location combo.
// Returns null if the role has no `hourlyFrom` baseline. UI hides the range
// gracefully in that case.
// 2026-06-10 (Loom 34): one fixed seniority ladder for every role, £7.50
// junior / £8.50 mid / £12 senior at the Philippines baseline, scaled by the
// location's low multiplier. Sub-£15 results round to the nearest 50p,
// larger ones to the nearest pound.
const SEN_FIXED_BASE = { junior: 7.5, mid: 8.5, senior: 12 };
const dfLadderRate = (base, loc) => {
  const x = base * loc.multLo;
  return x < 15 ? Math.round(x * 2) / 2 : Math.round(x);
};
// Rates can carry 50p steps, so print 7.5 as 7.50.
const dfRateFmt = (v) => (v % 1 === 0 ? String(v) : v.toFixed(2));
window.dfRateFmt = dfRateFmt;

function dfHourlyRange(role, locId) {
  if (!role || typeof role.hourlyFrom !== 'number') return null;
  const loc = FT_LOCATIONS.find(l => l.id === locId);
  if (!loc) return null;
  return {
    lo: dfLadderRate(SEN_FIXED_BASE.junior, loc),
    hi: dfLadderRate(SEN_FIXED_BASE.senior, loc),
  };
}
window.dfHourlyRange = dfHourlyRange;

const FT_SENIORITY = [
  // 2026-05-26: `pos` = position within the location's range (0 = lo bound,
  // 1 = hi bound). Drives the per-seniority hourly rate displayed under
  // each tile. Junior anchors to the location pill "from £X/hr" floor.
  { id: 'junior', label: 'Junior',  blurb: '1-3 yrs experience',         pos: 0   },
  { id: 'mid',    label: 'Mid',     blurb: '3-6 yrs experience',         pos: 0.5 },
  { id: 'senior', label: 'Senior',  blurb: '6+ yrs · lead-capable',      pos: 1.0 },
];

// 2026-05-26: indicative hourly rate for role × location × seniority.
// Returns a positive integer £/hr, or null if either input is missing.
function dfSeniorityRate(role, locId, senId) {
  if (!role || typeof role.hourlyFrom !== 'number') return null;
  const loc = FT_LOCATIONS.find(l => l.id === locId);
  if (!loc) return null;
  const base = SEN_FIXED_BASE[senId];
  if (!base) return null;
  return dfLadderRate(base, loc);
}
window.dfSeniorityRate = dfSeniorityRate;

window.DEDICATED_FT_LOCATIONS = FT_LOCATIONS;
window.DEDICATED_FT_SENIORITY = FT_SENIORITY;
window.DEDICATED_ROLE_RECS    = ROLE_RECS;

// 2026-06-03 (Loom 18): brief "what they do + works well with" copy per role,
// surfaced in the role-card tooltip. Falls back to the generic pricing note
// for any role without an entry.
const ROLE_INFO = {
  'ft-ai':        'Sets up Claude and Claude Code, builds MCP servers and automations across your stack, and trains your team to run them. Works well with a Full-Stack Marketer and a Lead Generation Expert.',
  'ft-mkt-ops':   'A versatile marketer who runs your campaigns, light design, content, and day-to-day marketing operations. Works well with a Graphic Designer and an AI Implementation and Training Specialist.',
  'ft-graphic':   'Designs branded graphics, social assets, and ad creative. Our senior Graphic Designers also cover UI and UX design and 3D design. Works well with a Full-Stack Marketer and a Video Editor.',
  'ft-video':     'Edits short and long-form video for social and ads. Our senior Video Editors also cover motion graphics and animation. Works well with a Graphic Designer and a Full-Stack Marketer.',
  'ft-sdr':       'Books qualified meetings through outbound calls, email, and LinkedIn. Works well with a Lead Generation Expert and an Account Manager.',
  'ft-leadgen':   'Builds targeted prospect lists and personalised outreach sequences. Works well with an SDR and an Account Manager.',
  'ft-am':        'Owns client relationships and finds upsell opportunities across your book. Works well with an SDR and a Lead Generation Expert.',
  'ft-exec':      'Manages scheduling, inbox, and admin so your leaders stay focused. Works well with a Full-Stack Marketer and an Account Manager.',
  'pt-2d':        'Designs branded graphics, social assets, and marketing visuals. Our senior Graphic Designers also cover UI and UX design and 3D design. Works well with a Video Editor and a Full-Stack Marketer.',
  'pt-video':     'Edits short and long-form video for social, ads, and YouTube. Our senior Video Editors also cover motion graphics and animation. Works well with a Graphic Designer and a Full-Stack Marketer.',
  'pt-copy':      'Writes your campaigns, website, and email outreach copy in your brand voice. Works well with a Graphic Designer and a Full-Stack Marketer.',
  'pt-web':       'Builds and maintains your website, landing pages, and integrations. Works well with a Graphic Designer and an AI Implementation and Training Specialist.',
  'pt-ai':        'Sets up Claude and Claude Code, builds MCP servers and automations across your tools, and trains your team to run them. Works well with a Graphic Designer and a Full-Stack Marketer.',
  'pt-fsm':       'Runs your campaigns, content, and day-to-day marketing across channels, and writes your copy. Works well with a Graphic Designer and an SEO Specialist.',
  'pt-marketer':  'Runs your campaigns, content, and day-to-day marketing across channels. Works well with a Graphic Designer and an SEO Specialist.',
  'pt-seo':       'Improves your search rankings with on-page, technical, and content SEO. Works well with a Full-Stack Marketer and a Graphic Designer.',
  'pt-appt':      'Books qualified meetings into your calendar through outbound outreach. Works well with a Sales Representative and a Full-Stack Marketer.',
  'pt-sales':     'Runs discovery and closes deals from your booked pipeline. Works well with an Appointment Setter and a Full-Stack Marketer.',
  'pt-va':        'Handles research, data entry, scheduling, and day-to-day admin. Works well with an Appointment Setter and a Full-Stack Marketer.',
  // 2026-06-12 (Loom 41 1:36): PT matched 1:1 to Full-Time, four new role bios.
  'pt-sdr':       'Books qualified meetings into your calendar through outbound calls, email, and LinkedIn. Works well with a Lead Generation Expert and an Account Manager.',
  'pt-leadgen':   'Builds targeted prospect lists and personalised outreach sequences. Works well with a Sales Development Representative and an Account Manager.',
  'pt-am':        'Owns client relationships and finds upsell opportunities across your book. Works well with a Sales Development Representative and a Lead Generation Expert.',
  'pt-exec':      'Manages scheduling, inbox, and admin so your leaders stay focused. Works well with a Full-Stack Marketer and an Account Manager.',
};
window.DEDICATED_ROLE_INFO = ROLE_INFO;

// Step status retained for compat with elsewhere in the app (sidebar/summary).
function dfStepStatus(selection) {
  const tier = selection?.tier;
  const roles = selection?.roles || [];
  const cfgs = selection?.roleConfigs || {};
  const s1 = !!tier;
  const s2 = s1 && roles.length > 0;
  const s3 = s2 && roles.every(rid => {
    const c = cfgs[rid] || {};
    if (tier === 'fulltime') return !!c.location && !!c.seniority;
    if (tier === 'parttime') return typeof c.days === 'number' && c.days >= 1;
    return false;
  });
  return [s1, s2, s3, s3];
}
window.dfStepStatus = dfStepStatus;

function dfRolesForTier(service, tier) {
  if (!service || !tier) return [];
  return service.roles?.[tier] || [];
}
// 2026-06-12 (Loom 41 0:30): rough day-rate ladder for the PT custom role,
// mid-band across the matched role list, shared with sections.v2.jsx via window.
const PT_CUSTOM_RATES = { d1: 375, d5: 340, d10: 330, custom: 320 };
window.DF_PT_CUSTOM_RATES = PT_CUSTOM_RATES;
function dfRoleById(service, tier, rid) {
  // 2026-06-09: synthetic record for the user-defined custom role (FT only).
  if (rid === 'ft-custom') return { id: 'ft-custom', name: 'Custom role', isCustom: true, hourlyFrom: 0 };
  // 2026-06-12 (Loom 41 0:30): the PT counterpart, priced from a rough day-rate ladder.
  if (rid === 'pt-custom') return { id: 'pt-custom', name: 'Custom role', isCustom: true, price: PT_CUSTOM_RATES.d1, unit: '/day', priceLabel: 'From £' + PT_CUSTOM_RATES.d1 + '/day', dayRates: PT_CUSTOM_RATES };
  return dfRolesForTier(service, tier).find(r => r.id === rid) || null;
}
window.dfRolesForTier = dfRolesForTier;
window.dfRoleById = dfRoleById;

// Helper: format £ with commas, no decimals.
function dfMoney(n) {
  return '£' + Math.round(n).toLocaleString('en-GB');
}

// ── TOP TIME TOGGLE ────────────────────────────────────────────────────────────────
function DFTimeToggle({ service, tier, onTier, hasSelection }) {
  if (!service.tiers || service.tiers.length < 2) return null;
  return (
    <div className="df-time-toggle" role="tablist" aria-label="Engagement type">
      {service.tiers.map(t => {
        const on = tier === t.id;
        const isFT = t.id === 'fulltime';
        return (
          <button
            key={t.id}
            type="button"
            role="tab"
            aria-selected={on}
            className={`df-time-toggle__btn ${on ? 'df-time-toggle__btn--on' : ''}`}
            onClick={() => onTier(t.id)}
          >
            <span className="df-time-toggle__icon" aria-hidden="true">
              {isFT ? (
                <svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round" strokeLinejoin="round">
                  <rect x="3" y="7" width="18" height="13" rx="2" />
                  <path d="M8 7V5a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2" />
                </svg>
              ) : (
                <svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round" strokeLinejoin="round">
                  <rect x="3" y="5" width="18" height="16" rx="2" />
                  <path d="M3 10h18M8 3v4M16 3v4" />
                </svg>
              )}
            </span>
            <div className="df-time-toggle__text">
              <div className="df-time-toggle__name">{t.name}</div>
              <div className="df-time-toggle__price">
                {isFT ? 'Custom · monthly subscription' : 'From £175/day · flexible'}
              </div>
            </div>
            {on && hasSelection && (
              <span className="df-time-toggle__check" aria-hidden="true">
                <window.Check size={12} />
              </span>
            )}
          </button>
        );
      })}
    </div>
  );
}

// ── PER-ROLE LOCATION RECOMMENDATIONS ─────────────────────────────────────────────────────
// Philippines = delivery / creative / technical roles (cost-efficient, strong talent pool).
// UK          = client-facing / sales roles (native English, same time zone as UK clients).
const ROLE_RECS = {
  'pt-fsm':       { cc: 'ph', label: 'Philippines' },
  'pt-2d':        { cc: 'ph', label: 'Philippines' },
  'pt-copy':      { cc: 'ph', label: 'Philippines' },
  'pt-video':     { cc: 'ph', label: 'Philippines' },
  'pt-motion':    { cc: 'ph', label: 'Philippines' },
  'pt-3d':        { cc: 'ph', label: 'Philippines' },
  'pt-web':       { cc: 'ph', label: 'Philippines' },
  'pt-ai':        { cc: 'ph', label: 'Philippines' },
  'pt-va':        { cc: 'ph', label: 'Philippines' },
  'pt-marketer':  { cc: 'ph', label: 'Philippines' },
  'pt-seo':       { cc: 'ph', label: 'Philippines' },
  'pt-appt':      { cc: 'ph', label: 'Philippines' },
  'ft-mkt-ops':   { cc: 'ph', label: 'Philippines' },
  'ft-graphic':   { cc: 'ph', label: 'Philippines' },
  'ft-copy':      { cc: 'ph', label: 'Philippines' },
  'ft-video':     { cc: 'ph', label: 'Philippines' },
  'ft-fullstack': { cc: 'ph', label: 'Philippines' },
  'ft-leadgen':   { cc: 'ph', label: 'Philippines' },
  'ft-ai':        { cc: 'ph', label: 'Philippines' },
  'ft-uiux':      { cc: 'ph', label: 'Philippines' },
  'ft-3d':        { cc: 'ph', label: 'Philippines' },
  'ft-vmotion':   { cc: 'ph', label: 'Philippines' },
  'ft-exec':      { cc: 'ph', label: 'Philippines' },
  'ft-va':        { cc: 'ph', label: 'Philippines' },
  'pt-sales':     { cc: 'gb', label: 'United Kingdom' },
  'ft-sdr':       { cc: 'za', label: 'South Africa' },
  'ft-csm':       { cc: 'gb', label: 'United Kingdom' },
  'ft-am':        { cc: 'gb', label: 'United Kingdom' },
  // 2026-06-12 (Loom 41): the new PT roles mirror their FT counterparts' recs.
  'pt-sdr':       { cc: 'za', label: 'South Africa' },
  'pt-leadgen':   { cc: 'ph', label: 'Philippines' },
  'pt-am':        { cc: 'gb', label: 'United Kingdom' },
  'pt-exec':      { cc: 'ph', label: 'Philippines' },
  // 2026-06-12 (review): the custom role recommends the Philippines too.
  'pt-custom':    { cc: 'ph', label: 'Philippines' },
};

// ── PER-COUNTRY CAPACITY (Loom 44 9:55 + Loom 41 4:47, design approved by
// Nicole 12 Jun) ─────────────────────────────────────────────────────────
// Per-role, per-country capacity map in the ROLE_RECS pattern, extended by
// data alone. States per country id: 'limited' | 'full'; anything omitted is
// available. `_default` applies to every role without its own entry. For now
// the UK runs limited across the board per Alexander ("maybe we'll have
// limited for UK"), and waitlisted roles (role.waitlist) read full in every
// country. Later this map can be fed from Airtable via /api/prices.
const ROLE_CAPACITY = {
  _default: { uk: 'limited' },
};
function dfCountryCapacity(role, locId) {
  if (!role) return 'available';
  if (role.waitlist) return 'full';
  const m = ROLE_CAPACITY[role.id] || ROLE_CAPACITY._default || {};
  return m[locId] || 'available';
}
window.dfCountryCapacity = dfCountryCapacity;

// 2026-06-09: per-role task lists shown as a tick grid in the expanded
// Part-Time config (DFDayRateTiles). Six tasks each, split across two columns.
const ROLE_TASKS = {
  'pt-ai':    ['Setting up Claude and Claude Code for your team', 'Creating MCP servers & agent tooling', 'Building and connecting APIs & integrations', 'Running terminal-based agent workflows', 'Integrating AI models into your stack', 'Training your team on the tools & workflows'],
  'pt-fsm':   ['Planning multi-channel campaigns', 'Email & lifecycle marketing', 'Paid social & search setup', 'Landing pages & conversion optimisation', 'Marketing automation & funnels', 'Analytics, tracking & reporting'],
  'pt-2d':    ['Branded graphics & social assets', 'Ad creative, banners & thumbnails', 'Pitch decks & presentations', 'UI & UX design', 'Light 3D & motion graphics', 'Brand & style guidelines'],
  'pt-video': ['Short-form social edits', 'Long-form & YouTube edits', 'Motion graphics & titles', 'Colour grading & sound mix', 'Subtitles & captions', 'Repurposing & resizing per channel'],
  'pt-seo':   ['Keyword & competitor research', 'On-page optimisation', 'Technical SEO audits', 'Content briefs & strategy', 'Link-building & outreach', 'Rank tracking & reporting'],
  'pt-appt':  ['Prospecting & list building', 'Outbound calls & follow-ups', 'Booking qualified meetings', 'Email & LinkedIn outreach', 'CRM updates & notes', 'Clean handover to your sales team'],
  'pt-sales': ['Running discovery & demo calls', 'Managing your sales pipeline', 'Handling objections & negotiation', 'Proposals & quotes', 'CRM hygiene & forecasting', 'Closing & handover to onboarding'],
  'pt-sdr':     ['Phone-first outbound from your scripts', 'Omnichannel sequences across phone, email & LinkedIn', 'Qualifying inbound enquiries', 'Booking meetings with reminders & confirmations', 'CRM logging & pipeline notes', 'Reporting on conversations & meetings booked'],
  'pt-leadgen': ['Targeted prospect list building', 'Contact enrichment & verification', 'Personalised outreach sequences', 'Account, trigger & buying-signal research', 'List hygiene, suppressions & CRM imports', 'Reply-rate reporting & targeting refinement'],
  'pt-am':      ['Day-to-day client communication & check-ins', 'Onboarding new clients to value', 'Upsell & cross-sell spotting', 'Account health tracking & churn flags', 'Scope, timeline & renewal coordination', 'CRM notes, stages & forecasts'],
  'pt-exec':    ['Calendar management & scheduling', 'Inbox triage with agreed rules', 'Meeting notes, agendas & follow-ups', 'Travel, expenses & invoicing admin', 'Research & document organisation', 'Task-list management & check-ins'],
};
window.DEDICATED_ROLE_TASKS = ROLE_TASKS;

// 2026-06-10 (Nicole): hidden rather than deleted, flip to restore.
// The task tick grid duplicates the role overlays' Typical work tab
// (Alexander asked for the banner in Loom 41 1:53, Nicole will own the
// justification if asked). The in-house comparison panel is retired per
// Loom 42.
const DF_SHOW_PT_TASK_GRID = false;
const DF_SHOW_INHOUSE_COMPARISON = false;

// ── ROLE OVERLAYS (Loom 33 2:06 + Nicole's spec) ─────────────────────────────
// Per-role clickable overlay content rendered through window.GMOverlayModal
// (sections.v2.jsx). Tabs: Who it’s for / Typical work / Works well with.
// FAQs render at the bottom regardless of tab (circled style). Copy mined from
// the gogorilla.com pricing-page role overlays and the Airtable pricing CMS,
// reworked to house style. Rolling out role by role; cards without an entry
// keep the plain pricing tooltip.
const ROLE_OVERLAYS = {
  'ft-ai': {
    title: 'AI Implementation & Training Specialist',
    intro: 'A dedicated specialist who builds AI and automation into your day-to-day operations, then trains your team to run it.',
    tabs: [
      { label: 'Who it’s for', blocks: [
        { t: 'p', x: 'For teams that know manual work is eating hours but lack the in-house capability to automate it properly.' },
        { t: 'checks', items: [
          'Founders who want AI working in the business, not just talked about',
          'Ops and revenue teams losing hours to copy-paste work between tools',
          'Companies that adopted AI tools nobody has set up properly yet',
          'Leaders who want their own team trained to run the automations, with no agency dependency',
        ] },
      ] },
      { label: 'Typical work', blocks: [
        { t: 'checks', items: [
          'Setting up Claude, Claude Code, and OpenClaw for your team',
          'Building MCP servers and agent tooling across your stack',
          'Connecting APIs and integrations, including data syncs and notifications',
          'Configuring CRM triggers, list management rules, and lead routing',
          'Building operational dashboards and reporting automations',
          'Training your team on the tools and workflows so the gains stick',
        ] },
      ] },
      { label: 'Works well with', blocks: [
        { t: 'sub', x: 'With a Full-Stack Marketer' },
        { t: 'p', x: 'Your specialist builds the machine, and a Full-Stack Marketer feeds it. Automated lead routing, lifecycle emails, and clean reporting only pay off when campaigns are filling the pipeline, and together they compound, so every campaign gets faster to launch and cheaper to run.' },
        { t: 'sub', x: 'With a Lead Generation Expert' },
        { t: 'p', x: 'Pair them, and your outbound runs itself. The Lead Generation Expert builds the lists and sequences whilst your AI specialist wires enrichment, scoring, and follow-up automation around them, so no lead sits untouched.' },
      ] },
    ],
    faqs: [
      { q: 'Which tools can they work with?', a: 'Most modern stacks. Zapier, Make, HubSpot, Salesforce, Airtable, and the leading AI platforms are all common ground, and we match your shortlist to the tools you already run.' },
      { q: 'How quickly can they start?', a: 'We shortlist candidates within 48 to 72 hours and most clients have their specialist fully embedded within 4 weeks.' },
      { q: 'What does the commitment look like?', a: 'Your placement runs as a monthly rolling contract with a 3-month minimum commitment to start. After the minimum, you can cancel any time with 28 days’ notice.' },
      { q: 'What if they are not the right fit?', a: 'Every placement carries a 90-day performance guarantee. If it is not working, we find a replacement at no additional cost.' },
    ],
  },
  'ft-mkt-ops': {
    title: 'Full-Stack Marketer',
    intro: 'One versatile marketer who runs your campaigns, content, and day-to-day marketing operations as an embedded member of your team.',
    tabs: [
      { label: 'Who it’s for', blocks: [
        { t: 'p', x: 'For companies that need marketing shipped every week but are not ready to build a 4-person team.' },
        { t: 'checks', items: [
          'Founders still doing their own marketing in the evenings',
          'Companies whose agency retainer feels heavy for the output they see',
          'Teams with a strategy in place but nobody to execute it consistently',
          'Businesses that want one hire to cover campaigns, content, and reporting',
        ] },
      ] },
      { label: 'Typical work', blocks: [
        { t: 'p', x: 'A broad role, so the work splits into 4 areas. Most weeks touch all of them.' },
        { t: 'subtabs', items: [
          { label: 'Campaigns', blocks: [ { t: 'checks', items: [
            'Planning multi-channel campaigns and owning the marketing calendar',
            'Setting up and running paid social and search campaigns',
            'Email and lifecycle marketing, from one-off sends to automated nurture',
            'Coordinating briefs and assets in your project management system',
          ] } ] },
          { label: 'Content & copy', blocks: [ { t: 'checks', items: [
            'Writing direct-response copy for ads, social, landing pages, and email',
            'Maintaining content calendars and scheduling across platforms',
            'Producing content briefs and tone of voice guidelines',
            'Light design work in Figma and Canva using your existing templates',
          ] } ] },
          { label: 'Web & conversion', blocks: [ { t: 'checks', items: [
            'Building and optimising landing pages for conversion',
            'Running A/B tests on pages, offers, and creative',
            'Improving on-page search visibility across your site',
            'Managing UTM tagging and asset QA so tracking stays clean',
          ] } ] },
          { label: 'Ops & reporting', blocks: [ { t: 'checks', items: [
            'Building marketing automations and funnels',
            'Producing competitor and audience snapshots',
            'Preparing performance dashboards and campaign digests',
            'Analytics and tracking so every channel reports honestly',
          ] } ] },
        ] },
      ] },
      { label: 'Works well with', blocks: [
        { t: 'sub', x: 'With a Graphic Designer' },
        { t: 'p', x: 'Copy and creative ship together. Your marketer briefs campaigns in the morning and a dedicated Graphic Designer turns them into on-brand assets the same week, so launches stop waiting on freelancers.' },
        { t: 'sub', x: 'With an AI Implementation & Training Specialist' },
        { t: 'p', x: 'Everything repeatable gets automated. Reporting, lead routing, and lifecycle emails run themselves, which frees your marketer to spend their hours on the campaigns that grow revenue.' },
      ] },
    ],
    faqs: [
      { q: 'Is one person really enough to cover all of this?', a: 'For most companies under 50 staff, yes. A strong Full-Stack Marketer covers the everyday 80% of marketing on their own, and when one area needs real depth, you add a specialist role alongside them rather than replacing them.' },
      { q: 'How quickly can they start?', a: 'We shortlist candidates within 48 to 72 hours and most clients have their specialist fully embedded within 4 weeks.' },
      { q: 'What does the commitment look like?', a: 'Your placement runs as a monthly rolling contract with a 3-month minimum commitment to start. After the minimum, you can cancel any time with 28 days’ notice.' },
      { q: 'What if they are not the right fit?', a: 'Every placement carries a 90-day performance guarantee. If it is not working, we find a replacement at no additional cost.' },
    ],
  },
  'ft-graphic': {
    title: 'Graphic Designer',
    intro: 'A dedicated designer who owns your daily creative output, from ad variants to brand collateral, with senior designers also covering UI, UX, and 3D.',
    tabs: [
      { label: 'Who it’s for', blocks: [
        { t: 'p', x: 'For teams that burn money on freelancers, or burn weeks waiting on them, every time they need creative.' },
        { t: 'checks', items: [
          'Companies running paid ads that constantly need fresh creative variants',
          'Brands whose visual identity drifts a little with every outsourced job',
          'Marketing teams producing social content daily rather than occasionally',
          'Founders pitching investors and clients with decks that need to look the part',
        ] },
      ] },
      { label: 'Typical work', blocks: [
        { t: 'checks', items: [
          'Designing ad and social variants at volume, including overlays, carousels, stories, and thumbnails',
          'Owning your brand libraries, templates, and design system as they evolve',
          'Producing web and digital layouts for landing pages, hero sections, and UI components',
          'Designing brand and marketing collateral such as brochures, proposals, and pitch decks',
          'Handling retouching, cut-outs, and scalable template setup in Figma',
          'Covering UI, UX, and 3D design at senior level where your brief calls for it',
        ] },
      ] },
      { label: 'Works well with', blocks: [
        { t: 'sub', x: 'With a Full-Stack Marketer' },
        { t: 'p', x: 'Design without a pipeline of briefs sits idle. A Full-Stack Marketer keeps campaigns moving, so your designer is always working on the asset that ships next, and nothing stalls in a feedback loop.' },
        { t: 'sub', x: 'With a Video Editor' },
        { t: 'p', x: 'Static and motion creative from one pod. Your designer sets the visual language, your editor carries it into video, and every channel ends up looking like one brand rather than 2 suppliers.' },
      ] },
    ],
    faqs: [
      { q: 'Can they match our existing brand?', a: 'Yes. Your designer works inside your brand guidelines from day one, and if those guidelines need tightening they will maintain and evolve the libraries as part of the role.' },
      { q: 'How quickly can they start?', a: 'We shortlist candidates within 48 to 72 hours and most clients have their specialist fully embedded within 4 weeks.' },
      { q: 'What does the commitment look like?', a: 'Your placement runs as a monthly rolling contract with a 3-month minimum commitment to start. After the minimum, you can cancel any time with 28 days’ notice.' },
      { q: 'What if they are not the right fit?', a: 'Every placement carries a 90-day performance guarantee. If it is not working, we find a replacement at no additional cost.' },
    ],
  },
  'ft-video': {
    title: 'Video Editor',
    intro: 'A dedicated editor who turns your raw footage and long-form content into a steady stream of platform-ready video.',
    tabs: [
      { label: 'Who it’s for', blocks: [
        { t: 'p', x: 'For brands that publish video weekly and feel the drag every time an edit queues behind someone else’s project.' },
        { t: 'checks', items: [
          'Companies repurposing podcasts, webinars, and events into social clips',
          'Paid media teams that need fresh video ad iterations every week',
          'Founders building a personal brand on YouTube, LinkedIn, or TikTok',
          'Marketing teams tired of per-project freelancer pricing and queues',
        ] },
      ] },
      { label: 'Typical work', blocks: [
        { t: 'checks', items: [
          'Producing short-form and long-form edits at volume, with subtitles, captions, and multi-ratio versions',
          'Managing a rolling edit queue in your project management tool and delivering against weekly plans',
          'Repurposing long-form content into clips and performance-ready ad iterations',
          'Handling colour balance, audio levelling, branded end cards, and simple motion titles',
          'Covering motion graphics and animation at senior level, including kinetic titles and logo stings',
          'Exporting platform-ready renders and Lottie files where needed',
        ] },
      ] },
      { label: 'Works well with', blocks: [
        { t: 'sub', x: 'With a Graphic Designer' },
        { t: 'p', x: 'One visual language across stills and motion. Your designer sets the look, your editor carries it through every cut, and thumbnails, end cards, and ad frames all ship from the same pod.' },
        { t: 'sub', x: 'With a Full-Stack Marketer' },
        { t: 'p', x: 'An editor is only as busy as the content plan behind them. A Full-Stack Marketer keeps the calendar and briefs flowing, so every week ends with video shipped rather than footage waiting.' },
      ] },
    ],
    faqs: [
      { q: 'Which platforms and formats do they cover?', a: 'All the usual suspects. Reels, TikTok, Shorts, YouTube long-form, paid ad formats, and podcast clips, each delivered in the right ratios and specs for the channel.' },
      { q: 'How quickly can they start?', a: 'We shortlist candidates within 48 to 72 hours and most clients have their specialist fully embedded within 4 weeks.' },
      { q: 'What does the commitment look like?', a: 'Your placement runs as a monthly rolling contract with a 3-month minimum commitment to start. After the minimum, you can cancel any time with 28 days’ notice.' },
      { q: 'What if they are not the right fit?', a: 'Every placement carries a 90-day performance guarantee. If it is not working, we find a replacement at no additional cost.' },
    ],
  },
  'ft-sdr': {
    title: 'Sales Development Representative',
    intro: 'A dedicated outbound rep who books qualified meetings into your calendar through calls, email, and LinkedIn.',
    tabs: [
      { label: 'Who it’s for', blocks: [
        { t: 'p', x: 'For teams with a proven offer that need consistent top-of-funnel activity without the cost of a UK sales hire.' },
        { t: 'checks', items: [
          'Founders doing their own prospecting in the gaps between client calls',
          'Sales teams whose closers spend half the week prospecting instead of closing',
          'Companies with target lists ready but no calling capacity',
          'Teams that tried outsourced lead generation and want a rep who actually learns the pitch',
        ] },
      ] },
      { label: 'Typical work', blocks: [
        { t: 'checks', items: [
          'Running phone-first outbound campaigns from your scripts and ideal customer profile',
          'Executing omnichannel sequences across phone, email, and LinkedIn',
          'Qualifying inbound enquiries and booking meetings with reminders and confirmations',
          'Attending daily stand-ups and call coaching sessions with your team',
          'Logging every call outcome in your CRM and keeping pipeline notes current',
          'Reporting weekly on conversations, meetings booked, and what the market is saying',
        ] },
      ] },
      { label: 'Works well with', blocks: [
        { t: 'sub', x: 'With a Lead Generation Expert' },
        { t: 'p', x: 'Calls convert when the list is right. Your Lead Generation Expert builds and enriches the prospect lists, your SDR works them relentlessly, and neither wastes hours on the other’s job.' },
        { t: 'sub', x: 'With an Account Manager' },
        { t: 'p', x: 'Meetings booked become revenue kept. The SDR fills the calendar, the Account Manager grows every account that closes, and your pipeline compounds at both ends.' },
      ] },
    ],
    faqs: [
      { q: 'Do they work our scripts or bring their own?', a: 'Both. They start from your scripts and ideal customer profile, then refine the talk tracks with your team in coaching sessions as the data comes in.' },
      { q: 'How quickly can they start?', a: 'We shortlist candidates within 48 to 72 hours and most clients have their specialist fully embedded within 4 weeks.' },
      { q: 'What does the commitment look like?', a: 'Your placement runs as a monthly rolling contract with a 3-month minimum commitment to start. After the minimum, you can cancel any time with 28 days’ notice.' },
      { q: 'What if they are not the right fit?', a: 'Every placement carries a 90-day performance guarantee. If it is not working, we find a replacement at no additional cost.' },
    ],
  },
  'ft-leadgen': {
    title: 'Lead Generation Expert',
    intro: 'A dedicated specialist who builds the targeted lists, research, and outreach sequences that keep your sales team in conversations.',
    tabs: [
      { label: 'Who it’s for', blocks: [
        { t: 'p', x: 'For teams whose outbound stalls not from lack of effort but from thin lists and generic messaging.' },
        { t: 'checks', items: [
          'Sales teams burning through bought lists with falling reply rates',
          'Founders who know their niche but lack the time to map it account by account',
          'Companies personalising outreach by hand, one prospect at a time',
          'Teams that need research depth behind every campaign, not just volume',
        ] },
      ] },
      { label: 'Typical work', blocks: [
        { t: 'checks', items: [
          'Building targeted prospect lists mapped to your ideal customer profile',
          'Enriching and verifying contact data so sequences land with real people',
          'Writing personalised outreach sequences for email and LinkedIn',
          'Researching accounts, triggers, and buying signals worth acting on',
          'Maintaining list hygiene, suppressions, and CRM imports',
          'Reporting on reply rates and refining targeting with your sales team',
        ] },
      ] },
      { label: 'Works well with', blocks: [
        { t: 'sub', x: 'With a Sales Development Representative' },
        { t: 'p', x: 'Lists without calls sit still. Your Lead Generation Expert hands the SDR clean, researched prospects every morning, and the SDR turns them into booked meetings the same week.' },
        { t: 'sub', x: 'With an Account Manager' },
        { t: 'p', x: 'New logos are only half the job. Your expert maps expansion contacts inside existing accounts and the Account Manager turns that research into upsells, so growth comes from both directions.' },
      ] },
    ],
    faqs: [
      { q: 'Which data tools do they use?', a: 'Ours or yours. They work with the leading prospecting databases and enrichment tools, and they plug into whatever CRM and sequencing stack you already run.' },
      { q: 'How quickly can they start?', a: 'We shortlist candidates within 48 to 72 hours and most clients have their specialist fully embedded within 4 weeks.' },
      { q: 'What does the commitment look like?', a: 'Your placement runs as a monthly rolling contract with a 3-month minimum commitment to start. After the minimum, you can cancel any time with 28 days’ notice.' },
      { q: 'What if they are not the right fit?', a: 'Every placement carries a 90-day performance guarantee. If it is not working, we find a replacement at no additional cost.' },
    ],
  },
  'ft-am': {
    title: 'Account Manager',
    intro: 'A dedicated manager who owns your client relationships, keeps revenue safe, and finds the upsell opportunities hiding in your book.',
    tabs: [
      { label: 'Who it’s for', blocks: [
        { t: 'p', x: 'For companies where the founder is still the account manager, or where delivery teams handle relationships in the gaps between projects.' },
        { t: 'checks', items: [
          'Agencies and service businesses juggling renewals across a growing book',
          'Founders spending their week on check-in calls instead of growth',
          'Teams losing expansion revenue because nobody owns the ask',
          'Companies where churn surprises arrive with the cancellation email',
        ] },
      ] },
      { label: 'Typical work', blocks: [
        { t: 'checks', items: [
          'Owning day-to-day client communication, check-ins, and quarterly reviews',
          'Running onboarding so new clients reach value fast',
          'Spotting and progressing upsell and cross-sell opportunities across your book',
          'Tracking account health and flagging risk before it becomes churn',
          'Coordinating with your delivery team on scope, timelines, and renewals',
          'Keeping the CRM current with notes, stages, and forecasts',
        ] },
      ] },
      { label: 'Works well with', blocks: [
        { t: 'sub', x: 'With a Sales Development Representative' },
        { t: 'p', x: 'One fills the front of the funnel, whilst the other protects everything behind it. New meetings keep arriving whilst every signed client gets a named owner from day one.' },
        { t: 'sub', x: 'With a Lead Generation Expert' },
        { t: 'p', x: 'Expansion is a research problem before it is a sales problem. Your Lead Generation Expert maps the contacts and signals inside each account, and your Account Manager walks in with the right conversation.' },
      ] },
    ],
    faqs: [
      { q: 'Can they work directly with our clients?', a: 'Yes. They join as a named member of your team, on your domain and your tools, and your clients deal with them exactly as they would an in-house account manager.' },
      { q: 'How quickly can they start?', a: 'We shortlist candidates within 48 to 72 hours and most clients have their specialist fully embedded within 4 weeks.' },
      { q: 'What does the commitment look like?', a: 'Your placement runs as a monthly rolling contract with a 3-month minimum commitment to start. After the minimum, you can cancel any time with 28 days’ notice.' },
      { q: 'What if they are not the right fit?', a: 'Every placement carries a 90-day performance guarantee. If it is not working, we find a replacement at no additional cost.' },
    ],
  },
  'ft-exec': {
    title: 'Executive Assistant',
    intro: 'A dedicated assistant who runs scheduling, inbox, and admin so your leadership team spends its hours where they count.',
    tabs: [
      { label: 'Who it’s for', blocks: [
        { t: 'p', x: 'For founders and executives whose calendar runs them, rather than the other way round.' },
        { t: 'checks', items: [
          'Leaders triaging email at midnight instead of thinking',
          'Founders double-booked across investors, clients, and team',
          'Executives losing hours to travel plans, expenses, and follow-ups',
          'Teams where meeting notes and actions evaporate by Friday',
        ] },
      ] },
      { label: 'Typical work', blocks: [
        { t: 'checks', items: [
          'Managing complex calendars, scheduling, and meeting coordination',
          'Triaging the inbox with agreed rules for what reaches you',
          'Preparing meeting notes, agendas, and follow-up actions',
          'Booking travel and handling expenses and invoicing admin',
          'Conducting research and keeping documents and trackers organised',
          'Managing an agreed task list through your team’s daily and weekly check-ins',
        ] },
      ] },
      { label: 'Works well with', blocks: [
        { t: 'sub', x: 'With a Full-Stack Marketer' },
        { t: 'p', x: 'Your assistant clears the diary, and your marketer fills the pipeline. Together, they hand a founder back whole days each week, and nothing booked or promised slips through.' },
        { t: 'sub', x: 'With an Account Manager' },
        { t: 'p', x: 'Client commitments stop falling in the cracks between calls. Your assistant captures every action, and the Account Manager owns every relationship, so follow-up becomes a system rather than a memory.' },
      ] },
    ],
    faqs: [
      { q: 'How do they handle confidential information?', a: 'Carefully and contractually. Every specialist works under a non-disclosure agreement with access controls you set, and as the employer of record, we handle vetting and compliance.' },
      { q: 'How quickly can they start?', a: 'We shortlist candidates within 48 to 72 hours and most clients have their specialist fully embedded within 4 weeks.' },
      { q: 'What does the commitment look like?', a: 'Your placement runs as a monthly rolling contract with a 3-month minimum commitment to start. After the minimum, you can cancel any time with 28 days’ notice.' },
      { q: 'What if they are not the right fit?', a: 'Every placement carries a 90-day performance guarantee. If it is not working, we find a replacement at no additional cost.' },
    ],
  },
  'pt-ai': {
    title: 'AI Implementation & Training Specialist',
    intro: 'An on-demand specialist who automates your workflows and wires AI into your tools, by the day rather than the month.',
    tabs: [
      { label: 'Who it’s for', blocks: [
        { t: 'p', x: 'For teams with a backlog of automation ideas and nobody free to build them.' },
        { t: 'checks', items: [
          'Founders who want their first real automations shipped this month',
          'Ops teams stitching tools together with exports and goodwill',
          'Companies testing AI use cases before committing to a full-time hire',
          'Teams that need an automation backlog cleared in focused day blocks',
        ] },
      ] },
      { label: 'Typical work', blocks: [
        { t: 'checks', items: [
          'Setting up Claude, Claude Code, and OpenClaw, with your team trained to run them',
          'Building MCP servers and agent tooling across your stack',
          'Connecting APIs and building integrations, including data syncs and notifications',
          'Setting up CRM triggers, list management, and lead routing',
          'Developing prompt frameworks, templated content generation, and QA workflows',
          'Building spreadsheet and dashboard automations in focused day blocks',
        ] },
      ] },
      { label: 'Works well with', blocks: [
        { t: 'sub', x: 'With a Full-Stack Marketer' },
        { t: 'p', x: 'Automation pays back fastest where campaigns already run. Your marketer keeps the pipeline busy whilst your specialist removes the manual steps around it, so the same hours produce more shipped work each month.' },
        { t: 'sub', x: 'With a Graphic Designer' },
        { t: 'p', x: 'Creative production has more repetition than anyone admits. Your specialist automates the resizing, versioning, and asset handoffs around your designer, who gets those hours back for the work that needs a human eye.' },
      ] },
    ],
    faqs: [
      { q: 'How many days should we book to start?', a: 'Most teams start with a 5-day block, enough to ship 1 or 2 complete automations end to end. The 10-day and custom blocks suit an ongoing backlog, and they carry better day rates.' },
      { q: 'How quickly can they start?', a: 'Usually within 1 to 2 business days of booking, subject to availability. Your specialist is already employed, vetted, and trained, so there is no recruitment cycle and no onboarding delay.' },
      { q: 'Can I change how many days I book?', a: 'Yes. Day blocks flex month to month, so you can scale up when there is more to ship and back down in quieter periods. Larger blocks carry better day rates, up to 15% off on blocks of 11 days or more.' },
      { q: 'Are they freelancers?', a: 'No. Every specialist is a permanent GoGorilla.com employee, dedicated to your work rather than juggling clients, with HR, payroll, and compliance handled by us as the employer of record.' },
    ],
  },
  'pt-fsm': {
    title: 'Full-Stack Marketer',
    intro: 'One versatile marketer who runs your campaigns, content, and day-to-day marketing in flexible day blocks.',
    tabs: [
      { label: 'Who it’s for', blocks: [
        { t: 'p', x: 'For companies that need senior marketing execution some days a week, not a full-time seat.' },
        { t: 'checks', items: [
          'Founders who need campaigns shipped without managing a hire',
          'Companies between marketers that cannot let momentum stall',
          'Teams testing a channel before committing full-time budget to it',
          'Businesses where marketing is a 2-day-a-week job currently done in zero',
        ] },
      ] },
      { label: 'Typical work', blocks: [
        { t: 'p', x: 'A broad role, so the work splits into 4 areas. Most weeks touch all of them.' },
        { t: 'subtabs', items: [
          { label: 'Campaigns', blocks: [ { t: 'checks', items: [
            'Planning multi-channel campaigns and owning the marketing calendar',
            'Setting up and running paid social and search campaigns',
            'Email and lifecycle marketing, from one-off sends to automated nurture',
            'Coordinating briefs and assets in your project management system',
          ] } ] },
          { label: 'Content & copy', blocks: [ { t: 'checks', items: [
            'Writing direct-response copy for ads, social, landing pages, and email',
            'Maintaining content calendars and scheduling across platforms',
            'Producing content briefs and tone of voice guidelines',
            'Light design work in Figma and Canva using your existing templates',
          ] } ] },
          { label: 'Web & conversion', blocks: [ { t: 'checks', items: [
            'Building and optimising landing pages for conversion',
            'Running A/B tests on pages, offers, and creative',
            'Improving on-page search visibility across your site',
            'Managing UTM tagging and asset QA so tracking stays clean',
          ] } ] },
          { label: 'Ops & reporting', blocks: [ { t: 'checks', items: [
            'Building marketing automations and funnels',
            'Producing competitor and audience snapshots',
            'Preparing performance dashboards and campaign digests',
            'Analytics and tracking so every channel reports honestly',
          ] } ] },
        ] },
      ] },
      { label: 'Works well with', blocks: [
        { t: 'sub', x: 'With a Graphic Designer' },
        { t: 'p', x: 'Campaigns move at the speed of creative. Book design days alongside marketing days and every campaign lands with assets ready, no freelancer queue in the middle.' },
        { t: 'sub', x: 'With an SEO Specialist' },
        { t: 'p', x: 'Paid fills the pipeline this quarter, and search compounds for the next. Your marketer captures demand whilst the SEO Specialist builds the rankings that make every lead cheaper over time.' },
      ] },
    ],
    faqs: [
      { q: 'Can the same person cover our paid, email, and content?', a: 'That is the point of the role. One senior generalist runs the week-to-week mix, and when a single channel needs real depth, you book a specialist day block alongside them.' },
      { q: 'How quickly can they start?', a: 'Usually within 1 to 2 business days of booking, subject to availability. Your specialist is already employed, vetted, and trained, so there is no recruitment cycle and no onboarding delay.' },
      { q: 'Can I change how many days I book?', a: 'Yes. Day blocks flex month to month, so you can scale up when there is more to ship and back down in quieter periods. Larger blocks carry better day rates, up to 15% off on blocks of 11 days or more.' },
      { q: 'Are they freelancers?', a: 'No. Every specialist is a permanent GoGorilla.com employee, dedicated to your work rather than juggling clients, with HR, payroll, and compliance handled by us as the employer of record.' },
    ],
  },
  'pt-2d': {
    title: 'Graphic Designer',
    intro: 'An on-demand designer for ad variants, social assets, and brand collateral, booked by the day.',
    tabs: [
      { label: 'Who it’s for', blocks: [
        { t: 'p', x: 'For teams whose design needs arrive in bursts rather than a steady 40-hour week.' },
        { t: 'checks', items: [
          'Marketing teams that need campaign creative batched in focused days',
          'Founders preparing investor decks and sales proposals that must look right',
          'Companies refreshing brand or web visuals each quarter',
          'Teams handling urgent design needs or tightly scoped tasks',
        ] },
      ] },
      { label: 'Typical work', blocks: [
        { t: 'checks', items: [
          'Developing creative variations for campaigns, including resizing, overlays, carousels, stories, and thumbnails',
          'Designing brand assets and marketing materials such as logos, brochures, investor proposals, and pitch decks',
          'Crafting landing page visuals, hero sections, custom UI components, and visual guidelines',
          'Handling retouching, complex cut-outs, and typography polish',
          'Preparing scalable design templates in Figma',
          'Covering UI, UX, and light 3D at senior level where the brief calls for it',
        ] },
      ] },
      { label: 'Works well with', blocks: [
        { t: 'sub', x: 'With a Video Editor' },
        { t: 'p', x: 'Stills and motion from the same shelf. Your designer sets the frames, thumbnails, and brand kit, your editor brings them to life, and every channel stays visually one brand.' },
        { t: 'sub', x: 'With a Full-Stack Marketer' },
        { t: 'p', x: 'A designer is most valuable with a full brief pipeline. Your marketer plans the campaigns and writes the copy, so every booked design day starts with something worth designing.' },
      ] },
    ],
    faqs: [
      { q: 'How much can they get through in a day?', a: 'A focused day typically covers a batch of ad variants, a deck pass, or the visuals for a landing page. Many teams book 5-day blocks to batch a fortnight of creative in one run.' },
      { q: 'How quickly can they start?', a: 'Usually within 1 to 2 business days of booking, subject to availability. Your specialist is already employed, vetted, and trained, so there is no recruitment cycle and no onboarding delay.' },
      { q: 'Can I change how many days I book?', a: 'Yes. Day blocks flex month to month, so you can scale up when there is more to ship and back down in quieter periods. Larger blocks carry better day rates, up to 15% off on blocks of 11 days or more.' },
      { q: 'Are they freelancers?', a: 'No. Every specialist is a permanent GoGorilla.com employee, dedicated to your work rather than juggling clients, with HR, payroll, and compliance handled by us as the employer of record.' },
    ],
  },
  'pt-video': {
    title: 'Video Editor',
    intro: 'An on-demand editor who turns footage into platform-ready video, booked in day blocks.',
    tabs: [
      { label: 'Who it’s for', blocks: [
        { t: 'p', x: 'For teams with more footage than finished video.' },
        { t: 'checks', items: [
          'Podcasters and event teams sitting on hours of unused recordings',
          'Paid media teams that need ad cuts iterated weekly',
          'Founders posting clips across Reels, TikTok, Shorts, and LinkedIn',
          'Companies that need editing in bursts around launches and campaigns',
        ] },
      ] },
      { label: 'Typical work', blocks: [
        { t: 'checks', items: [
          'Editing cut-downs for Reels, TikTok, and Shorts with hooks, subtitles, and aspect-ratio versions',
          'Polishing user-generated content and producing performance-ready video ads',
          'Repurposing podcasts, webinars, and event highlights into shareable clips',
          'Applying colour balance, audio levelling, branded end cards, and simple motion titles',
          'Covering motion graphics and animation at senior level',
          'Exporting platform-ready renders for every channel you publish on',
        ] },
      ] },
      { label: 'Works well with', blocks: [
        { t: 'sub', x: 'With a Graphic Designer' },
        { t: 'p', x: 'Thumbnails, end cards, and ad frames decide whether the edit gets watched. Book the two together, and every video ships with the static assets that earn the click.' },
        { t: 'sub', x: 'With a Full-Stack Marketer' },
        { t: 'p', x: 'An edit is only half the job, and distribution is the other half. Your marketer plans where each cut runs and what it must achieve, so editing days produce campaign results rather than files.' },
      ] },
    ],
    faqs: [
      { q: 'What do we need to provide?', a: 'Raw footage and a brief. That is it. Share files through your drive or ours, and your editor handles versioning, delivery specs, and the export list per channel.' },
      { q: 'How quickly can they start?', a: 'Usually within 1 to 2 business days of booking, subject to availability. Your specialist is already employed, vetted, and trained, so there is no recruitment cycle and no onboarding delay.' },
      { q: 'Can I change how many days I book?', a: 'Yes. Day blocks flex month to month, so you can scale up when there is more to ship and back down in quieter periods. Larger blocks carry better day rates, up to 15% off on blocks of 11 days or more.' },
      { q: 'Are they freelancers?', a: 'No. Every specialist is a permanent GoGorilla.com employee, dedicated to your work rather than juggling clients, with HR, payroll, and compliance handled by us as the employer of record.' },
    ],
  },
  'pt-seo': {
    title: 'SEO Specialist',
    intro: 'An on-demand specialist who improves your rankings with technical, on-page, and content work, by the day.',
    tabs: [
      { label: 'Who it’s for', blocks: [
        { t: 'p', x: 'For companies that know search should be a channel but have never given it consistent hours.' },
        { t: 'checks', items: [
          'Founders whose site has never had a proper technical audit',
          'Content teams publishing weekly without briefs or keyword direction',
          'Companies leaking rankings after a migration or redesign',
          'Teams building an ongoing search pipeline with flexible scheduling',
        ] },
      ] },
      { label: 'Typical work', blocks: [
        { t: 'checks', items: [
          'Optimising titles, meta descriptions, headings, and internal linking',
          'Running site health checks, indexability analysis, and Core Web Vitals recommendations',
          'Implementing JSON-LD schema for rich-result targeting',
          'Keyword and competitor research with content briefs to match',
          'Optimisation passes on existing content and topic discovery',
          'Link-building outreach, rank tracking, and reporting',
        ] },
      ] },
      { label: 'Works well with', blocks: [
        { t: 'sub', x: 'With a Full-Stack Marketer' },
        { t: 'p', x: 'Search strategy needs hands to execute it. The SEO Specialist sets the briefs and priorities, your marketer ships the content and pages, and rankings stop waiting for someone to find time.' },
        { t: 'sub', x: 'With a Graphic Designer' },
        { t: 'p', x: 'Pages that rank still have to convert. Your designer turns the SEO Specialist’s recommendations into fast, clean landing pages, so the traffic you win actually becomes pipeline.' },
      ] },
    ],
    faqs: [
      { q: 'How long until we see movement?', a: 'Technical fixes can show within weeks, whilst content and authority compound over months. The honest answer is that consistency wins, which is exactly what booked monthly day blocks give you.' },
      { q: 'How quickly can they start?', a: 'Usually within 1 to 2 business days of booking, subject to availability. Your specialist is already employed, vetted, and trained, so there is no recruitment cycle and no onboarding delay.' },
      { q: 'Can I change how many days I book?', a: 'Yes. Day blocks flex month to month, so you can scale up when there is more to ship and back down in quieter periods. Larger blocks carry better day rates, up to 15% off on blocks of 11 days or more.' },
      { q: 'Are they freelancers?', a: 'No. Every specialist is a permanent GoGorilla.com employee, dedicated to your work rather than juggling clients, with HR, payroll, and compliance handled by us as the employer of record.' },
    ],
  },
  'pt-appt': {
    title: 'Appointment Setter',
    intro: 'An on-demand setter who books qualified meetings into your calendar through phone-first outreach.',
    tabs: [
      { label: 'Who it’s for', blocks: [
        { t: 'p', x: 'For teams that need conversations started without hiring a full-time rep.' },
        { t: 'checks', items: [
          'Founders who close well but never find time to prospect',
          'Sales teams that need short-term coverage or trial outbound capacity',
          'Companies building a qualified pipeline over several weeks',
          'Teams with lists and scripts ready but no one dialling',
        ] },
      ] },
      { label: 'Typical work', blocks: [
        { t: 'checks', items: [
          'Conducting targeted phone outreach from your ideal customer profile and scripts',
          'Executing sequences across phone, email, and LinkedIn where required',
          'Qualifying inbound enquiries, booking calendars, and managing reminders and confirmations',
          'Keeping your CRM current with call outcomes, notes, and next steps',
          'Prospecting and list building between calling blocks',
          'Handing booked meetings to your sales team with clean context',
        ] },
      ] },
      { label: 'Works well with', blocks: [
        { t: 'sub', x: 'With a Sales Representative' },
        { t: 'p', x: 'One books, one closes. Your setter keeps the calendar full whilst the Sales Representative runs the demos and negotiations, a complete outbound desk in 2 day-rate bookings.' },
        { t: 'sub', x: 'With a Full-Stack Marketer' },
        { t: 'p', x: 'Cold calls warm up fast when marketing runs alongside. Your marketer builds awareness and nurture around the outreach, so more dials land on people who already know the name.' },
      ] },
    ],
    faqs: [
      { q: 'Whose phone system and number do they use?', a: 'Yours or ours, whichever fits. They can dial through your existing stack or through ours, and either way, every outcome lands in your CRM.' },
      { q: 'How quickly can they start?', a: 'Usually within 1 to 2 business days of booking, subject to availability. Your specialist is already employed, vetted, and trained, so there is no recruitment cycle and no onboarding delay.' },
      { q: 'Can I change how many days I book?', a: 'Yes. Day blocks flex month to month, so you can scale up when there is more to ship and back down in quieter periods. Larger blocks carry better day rates, up to 15% off on blocks of 11 days or more.' },
      { q: 'Are they freelancers?', a: 'No. Every specialist is a permanent GoGorilla.com employee, dedicated to your work rather than juggling clients, with HR, payroll, and compliance handled by us as the employer of record.' },
    ],
  },
  'pt-sales': {
    title: 'Sales Representative',
    intro: 'An on-demand closer who runs discovery, demos, and negotiations from your booked pipeline.',
    tabs: [
      { label: 'Who it’s for', blocks: [
        { t: 'p', x: 'For teams generating more conversations than they can properly work.' },
        { t: 'checks', items: [
          'Founders stuck in back-to-back demos instead of the business',
          'Companies needing focused cycles of demos and follow-ups',
          'Teams that need consistent deal progression week after week',
          'Businesses testing a dedicated closer before a permanent sales hire',
        ] },
      ] },
      { label: 'Typical work', blocks: [
        { t: 'checks', items: [
          'Leading discovery calls and product demonstrations with prospects from marketing or outbound',
          'Managing proposals, follow-ups, objection handling, and negotiation within agreed guardrails',
          'Handling CRM updates, pipeline notes, and next-step planning',
          'Coordinating with your team on pricing, contracts, and finalising agreements',
          'Forecasting honestly so you always know what is really closing',
          'Handing closed deals to onboarding with clean context',
        ] },
      ] },
      { label: 'Works well with', blocks: [
        { t: 'sub', x: 'With an Appointment Setter' },
        { t: 'p', x: 'A closer with an empty calendar is an expensive day rate. Pair them, and the setter keeps qualified meetings flowing whilst your rep spends every booked day actually closing.' },
        { t: 'sub', x: 'With a Full-Stack Marketer' },
        { t: 'p', x: 'Deals move faster when prospects arrive educated. Your marketer builds the case studies, pages, and nurture that warm each lead, and your rep walks into easier conversations.' },
      ] },
    ],
    faqs: [
      { q: 'How do they learn our product?', a: 'A focused ramp. They work from your demo recordings, documentation, and call shadowing in their first block, and most reps run their own demos within the first week of booked days.' },
      { q: 'How quickly can they start?', a: 'Usually within 1 to 2 business days of booking, subject to availability. Your specialist is already employed, vetted, and trained, so there is no recruitment cycle and no onboarding delay.' },
      { q: 'Can I change how many days I book?', a: 'Yes. Day blocks flex month to month, so you can scale up when there is more to ship and back down in quieter periods. Larger blocks carry better day rates, up to 15% off on blocks of 11 days or more.' },
      { q: 'Are they freelancers?', a: 'No. Every specialist is a permanent GoGorilla.com employee, dedicated to your work rather than juggling clients, with HR, payroll, and compliance handled by us as the employer of record.' },
    ],
  },
  // 2026-06-12 (Loom 41 1:36): PT overlays for the four roles new to the
  // matched list, adapted from their FT counterparts with day-block framing.
  'pt-sdr': {
    title: 'Sales Development Representative',
    intro: 'An on-demand outbound rep who books qualified meetings into your calendar through calls, email, and LinkedIn, by the day rather than the month.',
    tabs: [
      { label: 'Who it’s for', blocks: [
        { t: 'p', x: 'For teams with a proven offer that need top-of-funnel activity in focused day blocks, without the cost of a full-time sales hire.' },
        { t: 'checks', items: [
          'Founders doing their own prospecting in the gaps between client calls',
          'Sales teams whose closers spend half the week prospecting instead of closing',
          'Companies with target lists ready but no calling capacity',
          'Teams that need call days added in busy months and dialled back in quiet ones',
        ] },
      ] },
      { label: 'Typical work', blocks: [
        { t: 'checks', items: [
          'Running phone-first outbound campaigns from your scripts and ideal customer profile',
          'Executing omnichannel sequences across phone, email, and LinkedIn',
          'Qualifying inbound enquiries and booking meetings with reminders and confirmations',
          'Working your prospect lists in focused, scheduled day blocks',
          'Logging every call outcome in your CRM and keeping pipeline notes current',
          'Reporting on conversations, meetings booked, and what the market is saying',
        ] },
      ] },
      { label: 'Works well with', blocks: [
        { t: 'sub', x: 'With a Lead Generation Expert' },
        { t: 'p', x: 'Calls convert when the list is right. Your Lead Generation Expert builds and enriches the prospect lists, your SDR works them relentlessly, and neither wastes hours on the other’s job.' },
        { t: 'sub', x: 'With an Account Manager' },
        { t: 'p', x: 'Meetings booked become revenue kept. The SDR fills the calendar, the Account Manager grows every account that closes, and your pipeline compounds at both ends.' },
      ] },
    ],
    faqs: [
      { q: 'How many days should we book to start?', a: 'Most teams start with a 5-day block, enough for a consistent weekly calling rhythm that keeps your pipeline moving. The 10-day and custom blocks suit an ongoing outbound motion, and they carry better day rates.' },
      { q: 'How quickly can they start?', a: 'Usually within 1 to 2 business days of booking, subject to availability. Your specialist is already employed, vetted, and trained, so there is no recruitment cycle and no onboarding delay.' },
      { q: 'Can I change how many days I book?', a: 'Yes. Day blocks flex month to month, so you can scale up when there is more to ship and back down in quieter periods. Larger blocks carry better day rates, up to 15% off on blocks of 11 days or more.' },
      { q: 'Are they freelancers?', a: 'No. Every specialist is a permanent GoGorilla.com employee, dedicated to your work rather than juggling clients, with HR, payroll, and compliance handled by us as the employer of record.' },
    ],
  },
  'pt-leadgen': {
    title: 'Lead Generation Expert',
    intro: 'An on-demand specialist who builds the targeted lists, research, and outreach sequences that keep your sales team in conversations, by the day rather than the month.',
    tabs: [
      { label: 'Who it’s for', blocks: [
        { t: 'p', x: 'For teams whose outbound stalls not from lack of effort but from thin lists and generic messaging.' },
        { t: 'checks', items: [
          'Sales teams burning through bought lists with falling reply rates',
          'Founders who know their niche but lack the time to map it account by account',
          'Companies personalising outreach by hand, one prospect at a time',
          'Teams that need research depth added in day blocks ahead of each campaign',
        ] },
      ] },
      { label: 'Typical work', blocks: [
        { t: 'checks', items: [
          'Building targeted prospect lists mapped to your ideal customer profile',
          'Enriching and verifying contact data so sequences land with real people',
          'Writing personalised outreach sequences for email and LinkedIn',
          'Researching accounts, triggers, and buying signals worth acting on',
          'Maintaining list hygiene, suppressions, and CRM imports',
          'Reporting on reply rates and refining targeting with your sales team',
        ] },
      ] },
      { label: 'Works well with', blocks: [
        { t: 'sub', x: 'With a Sales Development Representative' },
        { t: 'p', x: 'Lists without calls sit still. Your Lead Generation Expert hands the SDR clean, researched prospects every morning, and the SDR turns them into booked meetings whilst the data stays fresh.' },
        { t: 'sub', x: 'With an Account Manager' },
        { t: 'p', x: 'New logos are only half the job. Your expert maps expansion contacts inside existing accounts, and the Account Manager turns that research into renewals and upsells.' },
      ] },
    ],
    faqs: [
      { q: 'How many days should we book to start?', a: 'Most teams start with a 5-day block, enough to build and enrich a campaign-ready list end to end. The 10-day and custom blocks suit always-on outbound, and they carry better day rates.' },
      { q: 'How quickly can they start?', a: 'Usually within 1 to 2 business days of booking, subject to availability. Your specialist is already employed, vetted, and trained, so there is no recruitment cycle and no onboarding delay.' },
      { q: 'Can I change how many days I book?', a: 'Yes. Day blocks flex month to month, so you can scale up when there is more to ship and back down in quieter periods. Larger blocks carry better day rates, up to 15% off on blocks of 11 days or more.' },
      { q: 'Are they freelancers?', a: 'No. Every specialist is a permanent GoGorilla.com employee, dedicated to your work rather than juggling clients, with HR, payroll, and compliance handled by us as the employer of record.' },
    ],
  },
  'pt-am': {
    title: 'Account Manager',
    intro: 'An on-demand manager who owns your client relationships, keeps revenue safe, and finds the upsell opportunities hiding in your book, by the day rather than the month.',
    tabs: [
      { label: 'Who it’s for', blocks: [
        { t: 'p', x: 'For companies where the founder is still the account manager, or where delivery teams handle relationships in the gaps between projects.' },
        { t: 'checks', items: [
          'Agencies and service businesses juggling renewals across a growing book',
          'Founders spending their week on check-in calls instead of growth',
          'Teams losing expansion revenue because nobody owns the ask',
          'Companies that need senior relationship cover a few days each month',
        ] },
      ] },
      { label: 'Typical work', blocks: [
        { t: 'checks', items: [
          'Owning day-to-day client communication, check-ins, and quarterly reviews',
          'Running onboarding so new clients reach value fast',
          'Spotting and progressing upsell and cross-sell opportunities across your book',
          'Tracking account health and flagging risk before it becomes churn',
          'Coordinating with your delivery team on scope, timelines, and renewals',
          'Keeping the CRM current with notes, stages, and forecasts',
        ] },
      ] },
      { label: 'Works well with', blocks: [
        { t: 'sub', x: 'With a Sales Development Representative' },
        { t: 'p', x: 'One fills the front of the funnel, whilst the other protects everything behind it. New meetings keep arriving as every closed account stays looked after.' },
        { t: 'sub', x: 'With a Lead Generation Expert' },
        { t: 'p', x: 'Expansion is a research problem before it is a sales problem. Your Lead Generation Expert maps the contacts and signals inside each account, and your Account Manager turns them into revenue.' },
      ] },
    ],
    faqs: [
      { q: 'Can they work directly with our clients?', a: 'Yes. Most clients introduce their Account Manager as part of the team, working on your domain and tools, with day blocks scheduled around your client calendar.' },
      { q: 'How quickly can they start?', a: 'Usually within 1 to 2 business days of booking, subject to availability. Your specialist is already employed, vetted, and trained, so there is no recruitment cycle and no onboarding delay.' },
      { q: 'Can I change how many days I book?', a: 'Yes. Day blocks flex month to month, so you can scale up when there is more to ship and back down in quieter periods. Larger blocks carry better day rates, up to 15% off on blocks of 11 days or more.' },
      { q: 'Are they freelancers?', a: 'No. Every specialist is a permanent GoGorilla.com employee, dedicated to your work rather than juggling clients, with HR, payroll, and compliance handled by us as the employer of record.' },
    ],
  },
  'pt-exec': {
    title: 'Executive Assistant',
    intro: 'An on-demand assistant who runs scheduling, inbox, and admin so your leadership team spends its hours where they count, by the day rather than the month.',
    tabs: [
      { label: 'Who it’s for', blocks: [
        { t: 'p', x: 'For founders and executives whose calendar runs them, rather than the other way round.' },
        { t: 'checks', items: [
          'Leaders triaging email at midnight instead of thinking',
          'Founders double-booked across investors, clients, and team',
          'Executives losing hours to travel plans, expenses, and follow-ups',
          'Teams that need reliable admin cover a few days each month',
        ] },
      ] },
      { label: 'Typical work', blocks: [
        { t: 'checks', items: [
          'Managing complex calendars, scheduling, and meeting coordination',
          'Triaging the inbox with agreed rules for what reaches you',
          'Preparing meeting notes, agendas, and follow-up actions',
          'Booking travel and handling expenses and invoicing admin',
          'Conducting research and keeping documents and trackers organised',
          'Managing an agreed task list through your team’s daily and weekly check-ins',
        ] },
      ] },
      { label: 'Works well with', blocks: [
        { t: 'sub', x: 'With a Full-Stack Marketer' },
        { t: 'p', x: 'Your assistant clears the diary, and your marketer fills the pipeline. Together they hand a founder back whole days each month.' },
        { t: 'sub', x: 'With an Account Manager' },
        { t: 'p', x: 'Client commitments stop falling in the cracks between calls. Your assistant captures every action, and the Account Manager keeps every relationship moving.' },
      ] },
    ],
    faqs: [
      { q: 'How do they handle confidential information?', a: 'Every specialist works under NDA with access agreed up front, on your tools and your permissions, so sensitive information stays inside your systems.' },
      { q: 'How quickly can they start?', a: 'Usually within 1 to 2 business days of booking, subject to availability. Your specialist is already employed, vetted, and trained, so there is no recruitment cycle and no onboarding delay.' },
      { q: 'Can I change how many days I book?', a: 'Yes. Day blocks flex month to month, so you can scale up when there is more to ship and back down in quieter periods. Larger blocks carry better day rates, up to 15% off on blocks of 11 days or more.' },
      { q: 'Are they freelancers?', a: 'No. Every specialist is a permanent GoGorilla.com employee, dedicated to your work rather than juggling clients, with HR, payroll, and compliance handled by us as the employer of record.' },
    ],
  },
};
window.DEDICATED_ROLE_OVERLAYS = ROLE_OVERLAYS;

// Span-based clickable info tip for role cards (same pattern as the lead
// sourcing tip in sections.v2.jsx). Lives inside the card's role="checkbox"
// div, so every handler stops propagation to keep the card from toggling.
// The overlay itself is NOT mounted here. Modal state is hoisted to
// DedicatedFlow (GorillaMatrix pattern) so portal events can never re-show
// this tip over the dialog.
function DFRoleInfoTip({ role, tipParas, onOpenOverlay }) {
  const [pos, setPos] = dfUseState(null);
  const wrapRef = React.useRef(null);
  const hideTimer = React.useRef(null);
  React.useEffect(() => {
    if (!pos) return undefined;
    const _close = () => setPos(null);
    const _reg = (window.__ggTipClosers || (window.__ggTipClosers = new Set()));
    _reg.add(_close);
    return () => { _reg.delete(_close); };
  }, [pos]);
  const show = () => {
    clearTimeout(hideTimer.current);
    if (!wrapRef.current) return;
    const r = wrapRef.current.getBoundingClientRect();
    const vw = window.innerWidth || 1024;
    const halfTip = 165; const PAD = 12;
    const rawX = r.left + r.width / 2;
    const clampedX = Math.min(Math.max(rawX, halfTip + PAD), vw - halfTip - PAD);
    setPos({ x: clampedX, y: r.top, caretShift: rawX - clampedX });
  };
  const scheduleHide = () => { hideTimer.current = setTimeout(() => setPos(null), 220); };
  const cancelHide = () => clearTimeout(hideTimer.current);
  const open = (e) => { e.preventDefault(); e.stopPropagation(); };
  return (
    <span ref={wrapRef} className="gm-tip-wrap df-role-tip-wrap" style={{ display: 'inline-flex', verticalAlign: 'middle' }} onClick={(e) => e.stopPropagation()} onMouseEnter={show} onMouseLeave={scheduleHide} onFocus={show} onBlur={scheduleHide}>
      <span role="button" tabIndex={0} className="info-tip-icon" aria-label={`About the ${role.name} role`}
        onClick={open}
        onKeyDown={(e) => { if (e.key === 'Enter' || e.key === ' ') { e.stopPropagation(); open(e); } }}>
        <svg xmlns="http://www.w3.org/2000/svg" width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true"><circle cx="12" cy="12" r="10"/><line x1="12" y1="16" x2="12" y2="12"/><line x1="12" y1="8" x2="12.01" y2="8"/></svg>
      </span>
      {pos && ReactDOM.createPortal(
        <span className="info-tip-portal info-tip-portal--above gm-tip-portal df-role-tip-portal" role="tooltip" style={{ left: pos.x, top: pos.y, '--info-tip-caret-shift': `${pos.caretShift}px` }} onMouseEnter={cancelHide} onMouseLeave={scheduleHide}>
          <span className="info-tip-portal__head">{role.name}</span>
          {tipParas.map((para, i) => <span key={i} className="gm-tip-portal__p">{para}</span>)}
          <button type="button" className="tip-overlay-link" onClick={(e) => { e.preventDefault(); e.stopPropagation(); setPos(null); if (onOpenOverlay) onOpenOverlay(); }}>Learn more</button>
        </span>,
        document.body
      )}
    </span>
  );
}


// ── ROLE CARD (glass) ────────────────────────────────────────────────────────────────────
// 2026-05-28 Batch 2 (#32 + #35):
//   #32 — HoverPortalTip pricing tooltip on each card. PT explains tiered day rates;
//          FT explains indicative hourly rate, confirmed after shortlist.
//   #35 — Outer <button> converted to <div role="checkbox"> so a real <button> remove
//          can be nested inside. Explicit × appears when the card is selected (on=true),
//          matching the × already present on summary-panel role lines.
function DFRoleCard({ role, on, isFocused, onToggle, onFocus, isFT, rec, onOpenInfo }) {
  const className = [
    'df-role-card',
    role.badge && 'df-role-card--badged',
    on && 'df-role-card--on',
    isFocused && 'df-role-card--focused',
    !on && 'df-role-card--dim',
  ].filter(Boolean).join(' ');

  // #32: tooltip text — PT shows tiered day rates; FT explains indicative hourly
  const _baseTip = isFT
    ? 'Hourly rate is indicative for the recommended location. Final monthly fee is confirmed after we shortlist candidates to your brief. Rates vary by location and seniority.'
    : (() => {
        const d1 = (role.dayRates && role.dayRates.d1) || role.price;
        const d5 = role.dayRates && role.dayRates.d5;
        const d10 = role.dayRates && role.dayRates.d10;
        const cust = role.dayRates && role.dayRates.custom;
        if (d1 && d5 && d10 && cust) {
          return `1 day: £${d1}/day · 5 days: £${d5}/day (10% off) · 10 days: £${d10}/day (12% off) · 11+ days: £${cust}/day (15% off). Final pricing confirmed on your scoping call.`;
        }
        return 'Day rate at 1-day rate. Discounts: 5-day block 10% off · 10-day block 12% off · 11+ days 15% off. Final pricing confirmed on your scoping call.';
      })();

  const _roleInfo = (window.DEDICATED_ROLE_INFO || {})[role.id];
  const tipText = _roleInfo || _baseTip;
  return (
    // #35: <div role="checkbox"> allows nesting a real <button> remove inside
    <div
      className={className}
      role="checkbox"
      aria-checked={on}
      tabIndex={0}
      onClick={() => onToggle()}
      onKeyDown={e => { if (e.key === ' ' || e.key === 'Enter') { e.preventDefault(); onToggle(); } }}
    >
      {/* #35: explicit × remove, only shown when card is selected */}
      {on && (
        <button
          type="button"
          className="df-role-card__remove"
          aria-label={`Remove ${role.name}`}
          onClick={e => { e.stopPropagation(); onToggle(); }}
        >
          <svg viewBox="0 0 12 12" width="8" height="8" fill="none" stroke="currentColor" strokeWidth="2.2" strokeLinecap="round" aria-hidden="true">
            <line x1="2" y1="2" x2="10" y2="10"/>
            <line x1="10" y1="2" x2="2" y2="10"/>
          </svg>
        </button>
      )}
      <span className={`df-role-card__check ${on ? 'is-on' : ''}`} aria-hidden="true">
        {on && <window.Check size={11} />}
      </span>
      <div className="df-role-card__name">
        {/* #32: pricing info tooltip. Last word + icon kept together (nowrap)
            so the info icon can never wrap alone onto its own line. */}
        {(() => {
          const _parts = role.name.trim().split(' ');
          const _last = _parts.pop();
          const _head = _parts.join(' ');
          return (
            <>
              {_head ? _head + ' ' : ''}
              <span className="df-role-card__name-tail">
                {_last}
                {(window.DEDICATED_ROLE_OVERLAYS || {})[role.id] ? (
                  /* Loom 33 2:06: grey info icon, blue on hover, click opens the
                     role overlay. The icon's own .info-tip-icon margin supplies
                     the standard 0.4rem gap, so the wrapper adds none. */
                  <DFRoleInfoTip
                    role={role}
                    tipParas={isFT ? (_roleInfo ? [_roleInfo, _baseTip] : [_baseTip]) : [_roleInfo || _baseTip]}
                    onOpenOverlay={() => { if (onOpenInfo) onOpenInfo(role.id); }}
                  />
                ) : window.HoverPortalTip && (
                  <window.HoverPortalTip
                    wrapClassName="df-role-tip-wrap"
                    wrapStyle={{marginLeft: '0.4rem', display: 'inline-flex', verticalAlign: 'middle'}}
                    tipClassName="dis-tip dis-tip--above"
                    tip={tipText}
                    placement="above"
                  >
                    <window.InfoIcon
                      className="df-role-card__tip-icon"
                      title={`About ${role.name} pricing`}
                      onClick={(e) => e.stopPropagation()}
                    />
                  </window.HoverPortalTip>
                )}
              </span>
            </>
          );
        })()}
      </div>
      {role.badge && (
        <img
          src={`assets/badges/${role.badge}.webp`}
          alt={role.badge === 'recommended' ? 'Recommended' : 'Most popular'}
          className="df-role-card__rec-banner"
          aria-hidden="true"
        />
      )}
      <div className="df-role-card__price">{role.priceLabel}</div>
      {/* 2026-06-12 (review): the card-level waiting-list pill retired as
          redundant, the Onboarding Availability box and the sidebar pill
          carry the signal. */}
      {rec && (
        <div className="df-role-card__rec">
          {/* 2026-06-11 (Loom 42 0:00): flags are back on the role cards,
              slicker than the country name. The name stays as alt and title. */}
          <span>Recommended: </span>
          <img
            src={`https://flagcdn.com/${rec.cc}.svg`}
            alt={rec.label}
            title={rec.label}
            className="df-role-card__rec-flag"
            width="20"
            height="14"
            loading="lazy"
          />
        </div>
      )}
      <span className="df-role-card__glass" aria-hidden="true"></span>
    </div>
  );
}

// ── PT DAY-RATE PLAN TILES ──────────────────────────────────────────────────────────────
// ── Per-role commitment toggle (3 / 6 / 12 mo) ──
// Reuses the .svc__commit-bar visual treatment (pill row, brand-blue
// selected state, orange save chip) so the per-role pills match the
// service-level commit toggle pixel-for-pixel. Lives inline in the
// role-form header.
function DFCommitToggle({ cfg, onPatch }) {
  const selected = cfg && cfg.commitId ? String(cfg.commitId) : '12';
  const COMMITS = (window.COMMITS || [
    { id: '3',  months: 3,  save: 0  },
    { id: '6',  months: 6,  save: 20 },
    { id: '12', months: 12, save: 40 },
  ]);
  return (
    // 2026-06-08: "Contract length" eyebrow above the month pills, matching
    // the "MINIMUM COMMITMENT PRICING" label other services show above their
    // commitment toggle (same .svc__cat styling + info tooltip).
    <div className="df-commit-wrap">
      <div className="svc__cat df-commit-eyebrow">
        Contract length
        {window.HoverPortalTip && (
          <window.HoverPortalTip wrapClassName="svc__commit-eyebrow-tip" tipClassName="dis-tip dis-tip--above" placement="above" tip={<span className="dis-tip__body">Choose how long you would like to commit to this role. A longer contract unlocks a better hourly rate, up to 40% off on a 12-month commitment.</span>}>
            <window.InfoIcon className="svc__commit-eyebrow-info" />
          </window.HoverPortalTip>
        )}
      </div>
      <div className="svc__commit-bar svc__commit-bar--corner df-commit-bar" role="radiogroup" aria-label="Role commitment length">
        {COMMITS.map(c => {
          const on = selected === c.id;
          return (
            <button
              key={c.id}
              type="button"
              role="radio"
              aria-checked={on}
              className={`svc__commit-bar-btn ${on ? 'is-on' : ''}`}
              onClick={() => onPatch({ commitId: c.id })}
            >
              <span className="svc__commit-bar-text">{c.months} mo</span>
              {c.save > 0 && (
                <span className="svc__commit-bar-save">&minus;{c.save}%</span>
              )}
            </button>
          );
        })}
      </div>
    </div>
  );
}

// 2026-06-19 (Nicole): animated count for the part-time numbers, matching the
// tier and channel prices, so figures roll when the recurring toggle, location
// or day count changes. Reuses the shared useAnimatedValue hook.
function DFAnimatedMoney({ value }) {
  const _ua = window.useAnimatedValue || ((x) => x);
  const v = _ua(Math.round(value || 0));
  return <>{dfMoney(v)}</>;
}

function DFDayRateTiles({ role, cfg, onPatch, tier, depositCard, ptBilling = 'recurring' }) {
  const baseRate = role.price || 0;
  // 2026-06-19 (Loom, Bearjoy): white-label partners resell part-time day-rate
  // resources at the 40% wholesale, so the tile shows the amber wholesale with
  // the RRP beside it, and a margin calculator lets them set their client price.
  const _wlMult = (window.getAgencyMultiplier && window.__lastBuildPageState) ? window.getAgencyMultiplier(window.__lastBuildPageState, 'dedicated-pt') : 1;
  const _isWl = typeof _wlMult === 'number' && _wlMult > 0 && _wlMult < 1;
  const _wlRate = (d) => (window.ggRound5 || ((x) => Math.round(x)))(d * _wlMult);
  // Recurring (default) saves 20% off the day rate vs a one-off block; reflect that
  // in the tiles, RRP and margin calc so they track the recurring toggle.
  const _recMult = ptBilling === 'oneoff' ? 1 : 0.8;
  // 2026-06-10: PT location selection (same design as FT). Philippines is the
  // base day rate (multLo 1.0); UK/SA scale the day rate by the FT location
  // floor multiplier so the chosen country drives estimate, deposit, and cart.
  const _ptLoc = cfg.location || 'philippines';
  const _ptLocMult = (FT_LOCATIONS.find(l => l.id === _ptLoc) || {}).multLo || 1;
  const _ptRecEntry = ROLE_RECS[role.id];
  const _ptRecLocId = _ptRecEntry ? (FT_LOCATIONS.find(l => l.cc === _ptRecEntry.cc) || {}).id : null;
  const days = typeof cfg.days === 'number' ? cfg.days : null;
  let selectedPlanId = null;
  if (days === 1) selectedPlanId = 'd1';
  else if (days === 5) selectedPlanId = 'd5';
  else if (days === 10) selectedPlanId = 'd10';
  else if (typeof days === 'number' && days >= 11) selectedPlanId = 'custom';
  const isCustom = selectedPlanId === 'custom';

  return (
    <div className="df-plan">
      {/* 2026-06-09: per-role task tick grid (one heading, two columns). */}
      {DF_SHOW_PT_TASK_GRID && (() => {
        const _tasks = (ROLE_TASKS[role.id] || []);
        if (!_tasks.length) return null;
        const _half = Math.ceil(_tasks.length / 2);
        const _cols = [_tasks.slice(0, _half), _tasks.slice(_half)];
        return (
          <div className="df-trust-grid df-trust-grid--tasks" role="list" aria-label={`What your ${role.name} does`}>
            <div className="df-trust-grid__heading">What your {role.name} does</div>
            {_cols.map((col, ci) => (
              <div key={'tc' + ci} className="df-trust-grid__col">
                <ul className="df-trust-grid__list">
                  {col.map((t, i) => (
                    <li key={ci + '-' + i} className="df-trust-grid__item" role="listitem">
                      <svg className="df-trust-grid__check" viewBox="0 0 16 16" width="13" height="13" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
                        <polyline points="3 8.5 6.5 12 13 4.5"/>
                      </svg>
                      <span className="df-trust-grid__label">{t}</span>
                    </li>
                  ))}
                </ul>
              </div>
            ))}
          </div>
        );
      })()}
      {/* 2026-06-10 (Nicole): the split starts at Office Location so the
          combined box runs the full height beside the fields and tiles. */}
      <div className="df-plan__split">
      <div className="df-plan__left">
      {/* 2026-06-12 (Loom 41 0:30): the PT custom role captures its name and
          brief here, then prices through the standard day-rate tiles below. */}
      {role.isCustom && (
        <React.Fragment>
          <div className="df-field">
            <label className="df-field__label">Role name</label>
            <input
              id="gg-custom-name-pt"
              type="text"
              className={`df-custom-text${cfg.customNameWarn ? ' df-custom-text--flagged' : ''}`}
              maxLength={60}
              placeholder="e.g. Bilingual Customer Success Lead"
              value={cfg.customName || ''}
              onChange={(e) => onPatch({ customName: e.target.value, customNameWarn: cfg.customNameWarn ? !(window.ggTextOk ? window.ggTextOk(e.target.value) : true) : false })}
              onBlur={() => { if (cfg.customName && window.ggTextOk && !window.ggTextOk(cfg.customName)) onPatch({ customNameWarn: true }); }}
            />
            {cfg.customNameWarn && <div className="df-field__warn">This role name appears to contain wording we are unable to accept. Please edit or rephrase it to continue.</div>}
          </div>
          {/* 2026-06-12 (review): the day rate is typed like the Full-Time
              hourly budget, the rough preset ladder retires. */}
          <div className="df-field">
            <label className="df-field__label">
              Day rate
              {window.HoverPortalTip && (
                <window.HoverPortalTip wrapClassName="df-loc-tip-wrap" wrapStyle={{marginLeft:'0.4rem',display:'inline-flex',verticalAlign:'middle'}} tipClassName="dis-tip dis-tip--above" placement="above" tip={"Your daily budget for this role, from £150 a day. We confirm the final rate after we shortlist candidates to your brief, and a realistic budget attracts a stronger shortlist."}>
                  <window.InfoIcon title="About the day rate" onClick={(e) => e.stopPropagation()} />
                </window.HoverPortalTip>
              )}
            </label>
            <div className="df-custom-rate">
              <span className="df-custom-rate__prefix">£</span>
              <input
                type="text"
                inputMode="numeric"
                className="df-custom-text df-custom-rate__input"
                placeholder="350"
                value={cfg.customRate || ''}
                onChange={(e) => onPatch({ customRate: e.target.value.replace(/[^0-9]/g, '') })}
                onBlur={() => { const v = parseFloat(cfg.customRate); if (v > 0 && v < 150) onPatch({ customRate: '150' }); }}
              />
              <span className="df-custom-rate__suffix">/day</span>
            </div>
          </div>
          <div className="df-field">
            <label className="df-field__label">Role description</label>
            <textarea
              id="gg-custom-brief-pt"
              className={`df-textarea df-custom-brief${cfg.customBriefWarn ? ' df-custom-text--flagged' : ''}`}
              rows={3}
              maxLength={1200}
              placeholder="e.g. 3+ years in B2B SaaS support, fluent Spanish and English, owns the inbound queue and runs onboarding calls, comfortable in HubSpot and Intercom."
              value={cfg.customBrief || ''}
              onChange={(e) => onPatch({ customBrief: e.target.value, customBriefWarn: cfg.customBriefWarn ? !(window.ggTextOk ? window.ggTextOk(e.target.value) : true) : false })}
              onBlur={() => { if (cfg.customBrief && window.ggTextOk && !window.ggTextOk(cfg.customBrief)) onPatch({ customBriefWarn: true }); }}
            />
            {cfg.customBriefWarn && <div className="df-field__warn">This role description appears to contain wording we are unable to accept. Please edit or rephrase it to continue.</div>}
          </div>
        </React.Fragment>
      )}
      {/* 2026-06-10: Office Location (same design as FT). Day rate scales by region. */}
      <div className="df-field df-field--pt-loc">
        <label className="df-field__label">
          Office Location
          {window.HoverPortalTip && (
            <window.HoverPortalTip
              wrapClassName="df-loc-tip-wrap"
              wrapStyle={{marginLeft: '0.4rem', display: 'inline-flex', verticalAlign: 'middle'}}
              tipClassName="dis-tip dis-tip--above"
              tip={"We have offices in the UK, the Philippines, and South Africa. Most specialists work remotely unless you add in-office access, so we never limit your talent pool to a single location. The day rate for the chosen location is shown, and the final rate is confirmed on your scoping call."}
              placement="above"
            >
              <window.InfoIcon title="About these rates" onClick={(e) => e.stopPropagation()} />
            </window.HoverPortalTip>
          )}
        </label>
        <div className="df-radio-row df-radio-row--loc">
          {FT_LOCATIONS.map(loc => {
            const on = _ptLoc === loc.id;
            const _from = Math.round((role.price || 0) * loc.multLo);
            const isRec = _ptRecLocId === loc.id;
            return (
              <button
                key={loc.id}
                type="button"
                className={`df-radio-pill df-radio-pill--loc ${on ? 'df-radio-pill--on' : ''}`}
                onClick={() => onPatch({ location: loc.id })}
                aria-pressed={on}
              >
                <span style={{display:'flex',alignItems:'center',gap:'8px'}}>
                  <span className="df-radio-pill__flag" aria-hidden="true">
                    <img
                      src={`https://flagcdn.com/${loc.cc}.svg`}
                      alt=""
                      className="df-radio-pill__flag-img"
                      width="20"
                      height="14"
                      loading="lazy"
                    />
                  </span>
                  <span>{loc.label}</span>
                </span>
                {!role.isCustom && <span className="df-radio-pill__range">From {dfMoney(_isWl ? _wlRate(Math.round(_from * _recMult)) : Math.round(_from * _recMult))}/day</span>}
                {isRec && (
                  <img
                    src="assets/badges/recommended.webp"
                    alt="Recommended"
                    className="df-location-rec-ribbon"
                    aria-hidden="true"
                  />
                )}
              </button>
            );
          })}
        </div>
      </div>
      {/* Loom 43 0:51: Day rate was "kind of obvious"; concise instruction
          instead, styled like the Office Location label. */}
      <div className="df-plan__rate-label">
        Select the number of days
        {window.HoverPortalTip && (
          <window.HoverPortalTip wrapClassName="df-loc-tip-wrap" wrapStyle={{marginLeft:'0.4rem',display:'inline-flex',verticalAlign:'middle'}} tipClassName="dis-tip dis-tip--above" placement="above" tip={"The daily rate for this role. Booking more days a month lowers the rate, and the final price is confirmed on your scoping call."}>
            <window.InfoIcon title="About day rates" onClick={(e) => e.stopPropagation()} />
          </window.HoverPortalTip>
        )}
      </div>

      <div className="df-plan__tiles df-plan__tiles--stacked">
        {DAY_PLANS.map(plan => {
          const on = selectedPlanId === plan.id;
          // 2026-06-12 (review): the custom role prices from the typed day
          // rate, flat across blocks and locations, like the FT budget.
          const _custRate = role.isCustom ? (parseFloat(cfg.customRate) || 0) : null;
          const explicit = role.dayRates && typeof role.dayRates[plan.id] === 'number'
            ? role.dayRates[plan.id]
            : null;
          const dayRate = _custRate != null
            ? Math.round(_custRate)
            : Math.round((explicit != null
              ? explicit
              : Math.round(baseRate * (1 - plan.discount))) * _ptLocMult);
          const _dispRate = Math.round(dayRate * _recMult);
          const total = _dispRate * (plan.id === 'custom' ? Math.max(11, days || 11) : plan.days);
          const baseDayRate = Math.round(((role.dayRates && typeof role.dayRates.d1 === 'number')
            ? role.dayRates.d1
            : baseRate) * _ptLocMult);
          const pct = _custRate != null ? 0 : (baseDayRate > 0
            ? Math.round((1 - dayRate / baseDayRate) * 100)
            : Math.round(plan.discount * 100));
          let blurb = plan.blurb;
          if (plan.id === 'd5' || plan.id === 'd10') {
            blurb = _custRate != null
              ? (dayRate > 0 ? `or a total of ${dfMoney(total)}` : 'Type a day rate above to price')
              : blurb.replace('{total}', dfMoney(total)).replace('{pct}', pct);
          }
          return (
            <button
              key={plan.id}
              type="button"
              className={`df-plan-tile ${on ? 'df-plan-tile--on' : ''} ${plan.id === 'custom' ? 'df-plan-tile--custom' : ''}`}
              onClick={() => {
                if (plan.id === 'custom') {
                  onPatch({ days: (typeof days === 'number' && days >= 11) ? days : 11 });
                } else {
                  onPatch({ days: plan.days });
                }
              }}
              aria-pressed={on}
            >
              <div className="df-plan-tile__head">
                <span className="df-plan-tile__name">{plan.label}</span>
                <span className={`df-plan-tile__radio ${on ? 'is-on' : ''}`} aria-hidden="true">
                  {on && <window.Check size={10} />}
                </span>
              </div>
              <div className="df-plan-tile__price">
                <span className="df-plan-tile__amt" style={_isWl ? { color: 'var(--gg-orange-deep, #C77800)' } : undefined}>{(role.isCustom && !(dayRate > 0)) ? '—' : <DFAnimatedMoney value={_isWl ? _wlRate(_dispRate) : _dispRate} />}</span>
                <span className="df-plan-tile__unit">/day</span>
                {_isWl && dayRate > 0 && <span className="df-plan-tile__rrp" style={{ color: 'var(--gg-blue, #002abf)', fontStyle: 'italic', fontWeight: 400, fontSize: '0.72em', marginLeft: '4px' }}>(RRP <DFAnimatedMoney value={_dispRate} />)</span>}
              </div>
              <div className="df-plan-tile__blurb">{blurb}</div>
            </button>
          );
        })}
      </div>
      </div>
      {/* 2026-06-12 (Loom 45, Nicole): when the custom block is active the
          days-per-month stepper rides inside the cost card above Estimated
          monthly, it no longer needs its own full-width line below the tiles. */}
      {depositCard && (
        <div className="df-plan__side">
          {isCustom
            ? React.cloneElement(depositCard, {
                daysSlotRoleId: role.id,
                daysSlot: <DFInCardDays role={role} days={days} onPatch={onPatch} baseRate={baseRate} ptLocMult={_ptLocMult} billing={ptBilling} rateOverride={role.isCustom ? (parseFloat(cfg.customRate) || 0) : null} />,
              })
            : depositCard}
        </div>
      )}
      </div>
      {_isWl && window.MarginRow && (() => {
        const _selPlan = DAY_PLANS.find(p => p.id === selectedPlanId) || DAY_PLANS.find(p => p.id === 'd5') || DAY_PLANS[0];
        const _selCust = role.isCustom ? (parseFloat(cfg.customRate) || 0) : null;
        const _selExp = (role.dayRates && typeof role.dayRates[_selPlan.id] === 'number') ? role.dayRates[_selPlan.id] : null;
        const _selRate = Math.round((_selCust != null ? Math.round(_selCust) : Math.round((_selExp != null ? _selExp : Math.round(baseRate * (1 - _selPlan.discount))) * _ptLocMult)) * _recMult);
        if (!(_selRate > 0)) return null;
        return (
          <div className="df-plan__margin" onClick={(e) => e.stopPropagation()} style={{ marginTop: '4px' }}>
            <window.MarginRow wholesale={_wlRate(_selRate)} rrp={_selRate} serviceId={`pt-${role.id}`} tierName={role.name} title={`${role.name} day-rate margin`} unitWord="day" flat autoFill monthlyTip="Your wholesale day rate as a white-label partner and the day rate you charge your client. Enter your price to see the margin you keep." />
          </div>
        );
      })()}

      {/* 2026-06-12 (Loom 45): the standalone custom-days line moved into the
          cost card's daysSlot above Estimated monthly. */}
      {/* 2026-06-10 (Nicole): availability on its own full-width line at the
          very bottom of the Part-Time config. */}
      {tier !== 'fulltime' && <DFAvailability tier={tier} forceFull={!!(role && role.waitlist)} countryCapacity={dfCountryCapacity(role, _ptLoc)} />}
    </div>
  );
}

// ── FT FORM ────────────────────────────────────────────────────────────────────────────────
// ── CUSTOM ROLE FORM ───────────────────────────────────────────────────────
// 2026-06-09: lets the client define a bespoke role — name, hourly rate, and a
// free-text requirements brief. Delivered from the Philippines (no location
// picker, no seniority / commitment / quantity). Estimate = rate x ~20 days x 8
// hours; the flat refundable deposit applies. All three inputs persist in
// roleConfigs['ft-custom'] and ride along in the saved-quote snapshot.
function DFCustomForm({ cfg, onPatch, depositCard, breakdown }) {
  // 2026-06-12 (Nicole): leaner form. The field subs are removed, the
  // shortlist note rides an Hourly rate tooltip with a £6/hour floor, and the
  // Philippines-only line gives way to a compact Office Location picker
  // sitting beside the rate (the Philippines stays recommended, not exclusive).
  const _loc = cfg.location || 'philippines';
  return (
    <div className="df-ft-form df-custom-form">
      <div className="df-ft-form__head">
        <div className="df-ft-form__title">Configure your custom role</div>
      </div>
      <div className="df-ft-split">
      <div className="df-ft-split__fields">
      <div className="df-field">
        <label className="df-field__label">Role name</label>
        <input
          id="gg-custom-name-ft"
          type="text"
          className={`df-custom-text${cfg.customNameWarn ? ' df-custom-text--flagged' : ''}`}
          maxLength={60}
          placeholder="e.g. Bilingual Customer Success Lead"
          value={cfg.customName || ''}
          onChange={(e) => onPatch({ customName: e.target.value, customNameWarn: cfg.customNameWarn ? !(window.ggTextOk ? window.ggTextOk(e.target.value) : true) : false })}
          onBlur={() => { if (cfg.customName && window.ggTextOk && !window.ggTextOk(cfg.customName)) onPatch({ customNameWarn: true }); }}
        />
        {cfg.customNameWarn && <div className="df-field__warn">This role name appears to contain wording we are unable to accept. Please edit or rephrase it to continue.</div>}
      </div>
      <div className="df-custom-row">
        <div className="df-field df-field--rate">
          <label className="df-field__label">
            Hourly rate
            {window.HoverPortalTip && (
              <window.HoverPortalTip wrapClassName="df-loc-tip-wrap" wrapStyle={{marginLeft:'0.4rem',display:'inline-flex',verticalAlign:'middle'}} tipClassName="dis-tip dis-tip--above" placement="above" tip={"Your hourly budget for this role, from £6 an hour. We confirm the final monthly fee after we shortlist candidates to your brief, and a realistic budget attracts a stronger shortlist."}>
                <window.InfoIcon title="About the hourly rate" onClick={(e) => e.stopPropagation()} />
              </window.HoverPortalTip>
            )}
          </label>
          <div className="df-custom-rate">
            <span className="df-custom-rate__prefix">£</span>
            <input
              type="text"
              inputMode="numeric"
              className="df-custom-text df-custom-rate__input"
              placeholder="8"
              value={cfg.customRate || ''}
              onChange={(e) => onPatch({ customRate: e.target.value.replace(/[^0-9]/g, '') })}
              onBlur={() => { const v = parseFloat(cfg.customRate); if (v > 0 && v < 6) onPatch({ customRate: '6' }); }}
            />
            <span className="df-custom-rate__suffix">/hour</span>
          </div>
        </div>
        <div className="df-field df-field--loc">
          <label className="df-field__label">
            Office Location
            {window.HoverPortalTip && (
              <window.HoverPortalTip wrapClassName="df-loc-tip-wrap" wrapStyle={{marginLeft:'0.4rem',display:'inline-flex',verticalAlign:'middle'}} tipClassName="dis-tip dis-tip--above" placement="above" tip={"We have offices in the UK, the Philippines, and South Africa, and the Philippines is our recommended pool for most custom roles. Availability and the final fee depend on the location you choose, confirmed on your scoping call."}>
                <window.InfoIcon title="About office locations" onClick={(e) => e.stopPropagation()} />
              </window.HoverPortalTip>
            )}
          </label>
          <div className="df-radio-row df-radio-row--loc df-radio-row--loc-compact">
            {FT_LOCATIONS.map(loc => {
              const on = _loc === loc.id;
              return (
                <button
                  key={loc.id}
                  type="button"
                  className={`df-radio-pill df-radio-pill--loc df-radio-pill--loc-sm ${on ? 'df-radio-pill--on' : ''}`}
                  onClick={() => onPatch({ location: loc.id })}
                  aria-pressed={on}
                >
                  <span className="df-radio-pill__flag" aria-hidden="true">
                    <img src={`https://flagcdn.com/${loc.cc}.svg`} alt="" className="df-radio-pill__flag-img" width="18" height="13" loading="lazy" />
                  </span>
                  <span>{loc.label}</span>
                  {loc.id === 'philippines' && (
                    <img src="assets/badges/recommended.webp" alt="Recommended" className="df-location-rec-ribbon" aria-hidden="true" />
                  )}
                </button>
              );
            })}
          </div>
        </div>
      </div>
      <div className="df-field">
        <label className="df-field__label">Role description</label>
        <textarea
          id="gg-custom-brief-ft"
          className={`df-textarea df-custom-brief${cfg.customBriefWarn ? ' df-custom-text--flagged' : ''}`}
          rows={4}
          maxLength={1200}
          placeholder="e.g. 3+ years in B2B SaaS support, fluent Spanish and English, owns the inbound queue and runs onboarding calls, comfortable in HubSpot and Intercom."
          value={cfg.customBrief || ''}
          onChange={(e) => onPatch({ customBrief: e.target.value, customBriefWarn: cfg.customBriefWarn ? !(window.ggTextOk ? window.ggTextOk(e.target.value) : true) : false })}
          onBlur={() => { if (cfg.customBrief && window.ggTextOk && !window.ggTextOk(cfg.customBrief)) onPatch({ customBriefWarn: true }); }}
        />
        {cfg.customBriefWarn && <div className="df-field__warn">This role description appears to contain wording we are unable to accept. Please edit or rephrase it to continue.</div>}
      </div>
      {breakdown}
      </div>
      <div className="df-ft-split__side">{depositCard}</div>
      </div>
    </div>
  );
}

function DFFullTimeForm({ role, cfg, onPatch, depositCard, breakdown }) {
  // 2026-06-03: full-time commitment discount (3mo 0% / 6mo -20% / 12mo -40%)
  // applies to the DISPLAYED hourly rates (struck-through original + net rate).
  // Estimate stays "Custom" and full-time is not added to the binding total.
  // 2026-06-10: contract-length toggle removed. Flat 3-month minimum
  // commitment for all FT roles; no commitment discounts (stale commitId
  // values in localStorage are ignored).
  const _ftCommitMult = 1.0;
  const _ftCommitPct = 0;
  const recEntry = ROLE_RECS[role.id];
  const recLocId = recEntry
    ? (FT_LOCATIONS.find(l => l.cc === recEntry.cc) || {}).id
    : null;
  return (
    <div className="df-ft-form">
      <div className="df-ft-form__head">
        <div className="df-ft-form__title">Configure your {role.name} placement{window.HoverPortalTip && (<window.HoverPortalTip wrapClassName="df-loc-tip-wrap" wrapStyle={{marginLeft:'0.4rem',display:'inline-flex',verticalAlign:'middle'}} tipClassName="dis-tip dis-tip--above" tip={<><span className="dis-tip__body" style={{display:'block'}}>Every role is priced on the same ladder, set by the seniority and location you choose.</span><span className="dis-tip__body" style={{display:'block',marginTop:'0.55em'}}>On United Kingdom and South Africa placements, a talent buyout is also available if you later wish to hire the specialist directly, at an industry-standard 20% of annual salary in South Africa and 15% in the UK.</span></>} placement="above"><window.InfoIcon title="About role pricing" onClick={(e) => e.stopPropagation()} /></window.HoverPortalTip>)}</div>
        {/* 2026-06-10: static note replaces the 3/6/12mo contract toggle */}
        <div className="df-commit-wrap">
          <div className="svc__cat df-commit-eyebrow">
            3-month minimum commitment
            {window.HoverPortalTip && (
              <window.HoverPortalTip wrapClassName="svc__commit-eyebrow-tip" tipClassName="dis-tip dis-tip--above" placement="above" tip={<><span className="dis-tip__body" style={{display:'block'}}>Your placement runs as a monthly rolling contract with a 3-month minimum commitment to start.</span><span className="dis-tip__body" style={{display:'block',marginTop:'0.55em'}}>After the minimum, you can cancel any time with 28 days’ notice.</span></>}>
                <window.InfoIcon className="svc__commit-eyebrow-info" />
              </window.HoverPortalTip>
            )}
          </div>
        </div>
      </div>
      {/* 2026-06-10 (Nicole review): config splits 2-up, Location + Seniority
          on the left, the In-House | Direct Hire cost card filling the empty
          right half. The card stacks internally in this narrow column. */}
      <div className="df-ft-split">
      <div className="df-ft-split__fields">
      <div className="df-field">
        <label className="df-field__label">
          Office Location
          {window.HoverPortalTip && (
            <window.HoverPortalTip
              wrapClassName="df-loc-tip-wrap"
              wrapStyle={{marginLeft: '0.4rem', display: 'inline-flex', verticalAlign: 'middle'}}
              tipClassName="dis-tip dis-tip--above"
              tip={"We have offices in the UK, the Philippines, and South Africa. Most specialists work remotely unless you add in-office access, so we never limit your talent pool to a single location. The floor rate for the chosen location is shown, and the final monthly fee is confirmed after we shortlist candidates to your brief."}
              placement="above"
            >
              <window.InfoIcon title="About these rates" onClick={(e) => e.stopPropagation()} />
            </window.HoverPortalTip>
          )}
        </label>
        {/* df-radio-row--loc gives all location pills uniform top padding so content aligns */}
        <div className="df-radio-row df-radio-row--loc">
          {FT_LOCATIONS.map(loc => {
            const on = cfg.location === loc.id;
            const range = dfHourlyRange(role, loc.id);
            const isRec = recLocId === loc.id;
            return (
              <button
                key={loc.id}
                type="button"
                className={`df-radio-pill df-radio-pill--loc ${on ? 'df-radio-pill--on' : ''}`}
                onClick={() => onPatch({ location: loc.id })}
                aria-pressed={on}
              >
                <span style={{display:'flex',alignItems:'center',gap:'8px'}}>
                  <span className="df-radio-pill__flag" aria-hidden="true">
                    <img
                      src={`https://flagcdn.com/${loc.cc}.svg`}
                      alt=""
                      className="df-radio-pill__flag-img"
                      width="20"
                      height="14"
                      loading="lazy"
                    />
                  </span>
                  <span>{loc.label}</span>
                </span>
                <span className="df-radio-pill__range">
                  From £{dfRateFmt(range ? range.lo : loc.hourlyFrom)}/hour
                </span>
                {isRec && (
                  <img
                    src="assets/badges/recommended.webp"
                    alt="Recommended"
                    className="df-location-rec-ribbon"
                    aria-hidden="true"
                  />
                )}
              </button>
            );
          })}
        </div>
      </div>

      <div className="df-field">
        <label className="df-field__label">
          Seniority
          {window.HoverPortalTip && (
            <window.HoverPortalTip
              wrapClassName="df-loc-tip-wrap"
              wrapStyle={{marginLeft: '0.4rem', display: 'inline-flex', verticalAlign: 'middle'}}
              tipClassName="dis-tip dis-tip--above"
              tip={"We match candidates to your chosen experience tier. Every candidate goes through thorough reference checks and vetting, and in some cases comes from our in-house team, so you get proven quality at the level you need."}
              placement="above"
            >
              <window.InfoIcon title="About seniority and vetting" onClick={(e) => e.stopPropagation()} />
            </window.HoverPortalTip>
          )}
        </label>
        <div className="df-radio-row">
          {FT_SENIORITY.map(s => {
            const on = cfg.seniority === s.id;
            const rate = dfSeniorityRate(role, cfg.location, s.id);
            return (
              <button
                key={s.id}
                type="button"
                className={`df-radio-pill df-radio-pill--col ${on ? 'df-radio-pill--on' : ''}`}
                onClick={() => onPatch({ seniority: s.id })}
                aria-pressed={on}
              >
                <span className="df-sen__main">
                  <span className="df-radio-pill__name">{s.label}</span>
                  <span className="df-radio-pill__blurb">{s.blurb}</span>
                  {rate != null && (
                    <span className="df-radio-pill__rate">From £{dfRateFmt(rate)}/hour</span>
                  )}
                </span>
                {/* 2026-06-08: hire-quantity lives INSIDE the selected seniority
                    card. Always rendered (collapsed) so it animates open when
                    the card is selected; the card appears to expand right. Uses
                    span role=button so it never nests <button> elements. */}
                <span className="df-sen__qty" onClick={(e) => e.stopPropagation()} aria-hidden={!on}>
                  <span className="df-ft-qty__label">How many hires?</span>
                  <span className="df-ft-qty__ctrl">
                    <span role="button" tabIndex={on ? 0 : -1} className={`df-ft-qty__btn ${(cfg.qty || 1) <= 1 ? 'df-ft-qty__btn--off' : ''}`} aria-label="Decrease number of hires" onClick={(e) => { e.stopPropagation(); if ((cfg.qty || 1) > 1) onPatch({ qty: (cfg.qty || 1) - 1 }); }} onKeyDown={(e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); e.stopPropagation(); if ((cfg.qty || 1) > 1) onPatch({ qty: (cfg.qty || 1) - 1 }); } }}>&minus;</span>
                    <span className="df-ft-qty__num" aria-live="polite">{cfg.qty || 1}</span>
                    <span role="button" tabIndex={on ? 0 : -1} className={`df-ft-qty__btn ${(cfg.qty || 1) >= 99 ? 'df-ft-qty__btn--off' : ''}`} aria-label="Increase number of hires" onClick={(e) => { e.stopPropagation(); if ((cfg.qty || 1) < 99) onPatch({ qty: (cfg.qty || 1) + 1 }); }} onKeyDown={(e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); e.stopPropagation(); if ((cfg.qty || 1) < 99) onPatch({ qty: (cfg.qty || 1) + 1 }); } }}>+</span>
                  </span>
                </span>
                {/* 2026-06-10 (Loom 34): Recommended badge on the Mid tier,
                    same half-out centred ribbon as the Philippines location
                    pill. The --col pills already carry the thin glass bevel. */}
                {s.id === 'mid' && (
                  <img
                    src="assets/badges/recommended.webp"
                    alt="Recommended"
                    className="df-location-rec-ribbon"
                    aria-hidden="true"
                  />
                )}
              </button>
            );
          })}
        </div>
      </div>
      {breakdown}
      </div>
      <div className="df-ft-split__side">{depositCard}</div>
      </div>
      {/* 2026-06-12 (Nicole): Full-Time gains the Onboarding Availability box
          at the bottom like Part-Time, reading the chosen office location. */}
      <DFAvailability tier="fulltime" forceFull={!!(role && role.waitlist)} countryCapacity={dfCountryCapacity(role, cfg.location)} />
    </div>
  );
}

// ── ONBOARDING AVAILABILITY ────────────────────────────────────────────────
// ── START WINDOW (Loom 41) ───────────────────────────────────────────
// 2026-06-12: the date-picker MVP substitute. Collects how soon the client
// needs their specialist(s) so the proposal and bench prep can move whilst
// the call is arranged. The quote stays non-binding, nothing is booked here.
const DF_START_WINDOWS = [
  { id: 'asap',      label: 'Within 2 weeks' },
  { id: 'month',     label: 'Within a month' },
  { id: 'quarter',   label: '1 to 3 months' },
  { id: 'exploring', label: 'Just exploring' },
];
window.DF_START_WINDOW_LABELS = Object.fromEntries(DF_START_WINDOWS.map(o => [o.id, o.label]));
function DFStartWindow({ value, onSet }) {
  return (
    <div className="df-start df-start--card">
      <div className="df-start__head">
        <span className="df-start__title">How soon do you need them to start?</span>
        {window.HoverPortalTip && (
          <window.HoverPortalTip wrapClassName="df-loc-tip-wrap" wrapStyle={{display:'inline-flex',verticalAlign:'middle',marginLeft:'0.4rem'}} tipClassName="dis-tip dis-tip--above" placement="above" tip={"Let us know how soon you would like your specialist to start and we will do our best to line things up before your call. Start dates depend on availability, so treat this as a helpful guide for your proposal rather than a guarantee."}>
            <window.InfoIcon />
          </window.HoverPortalTip>
        )}
      </div>
      <div className="df-start__opts" role="radiogroup" aria-label="How soon do you need them to start">
        {DF_START_WINDOWS.map(o => (
          <button key={o.id} type="button" role="radio" aria-checked={value === o.id} className={`df-start__opt thin-glass-frame ${value === o.id ? 'df-start__opt--on' : ''}`} onClick={() => onSet && onSet(value === o.id ? null : o.id)}>
            {o.label}
          </button>
        ))}
      </div>
    </div>
  );
}

function DFAvailability({ tier, forceFull, countryCapacity }) {
  // §3.6, qualifier-aware capacity status. urgency=now shifts to Limited
  // (proxy for the spec's hiringTime='within-14-days'). Falls back to the
  // legacy addonAvailability helper when getDrFtTalentCapacity isn't loaded
  // (initial render race) or when tier is PT.
  const isFT = tier === 'fulltime';
  let status = 'open';
  let caption = 'We currently have capacity to onboard new clients for this service. We recommend scheduling a call soon to secure your spot, as we will move to a waiting list once our capacity limit is reached to ensure quality for our existing clients.';
  if (isFT && typeof window.getDrFtTalentCapacity === 'function') {
    const cap = window.getDrFtTalentCapacity(window.__lastBuildPageState);
    status = cap.status || 'open';
    caption = cap.caption || caption;
  } else if (window.addonAvailability) {
    const cap = window.addonAvailability({ id: 'dedicated-resources', badge: 'recommended' });
    status = cap.status || 'open';
    caption = cap.caption || caption;
  }
  // 2026-06-12 (Loom 44 9:55): the chosen office location drives the state.
  // 'limited' softens an open status, 'full' matches the waitlist treatment.
  if (countryCapacity === 'limited' && status === 'open') {
    status = 'limited';
    caption = 'Capacity for this role is currently limited in your selected office location. We recommend scheduling a call soon to secure your spot, or choosing another location for faster onboarding.';
  }
  if (countryCapacity === 'full') {
    status = 'full';
    caption = 'This role is currently at capacity in your selected office location. Adding it reserves your place on the waiting list, and we email you as soon as a spot opens, typically within 2 to 4 weeks.';
  }
  // 2026-06-12 (Loom 41): a waitlisted focused role forces the full state.
  if (forceFull) {
    status = 'full';
    caption = 'This role is currently at capacity. Adding it reserves your place on the waiting list, and we email you as soon as a spot opens, typically within 2 to 4 weeks.';
  }
  return (
    <div className={`df-avail capacity capacity--${status}`}>
      <div className="df-avail__head">
        <span className="df-avail__title">Onboarding Availability:</span>
        <span className="df-avail__status">
          {status === 'open' && (
            <svg viewBox="0 0 16 16" width="13" height="13" fill="none" stroke="currentColor" strokeWidth="2" aria-hidden="true">
              <circle cx="8" cy="8" r="6.5" />
              <path d="M5 8.5l2 2L11.5 6" />
            </svg>
          )}
          {status === 'limited' && (
            <svg viewBox="0 0 16 16" width="13" height="13" fill="none" stroke="currentColor" strokeWidth="2" aria-hidden="true">
              <circle cx="8" cy="8" r="6.5" />
              <line x1="8" y1="5" x2="8" y2="9" strokeLinecap="round" />
              <circle cx="8" cy="11.5" r="0.6" fill="currentColor" stroke="none" />
            </svg>
          )}
          {status === 'closed' && (
            <svg viewBox="0 0 16 16" width="13" height="13" fill="none" stroke="currentColor" strokeWidth="2" aria-hidden="true">
              <circle cx="8" cy="8" r="6.5" />
              <line x1="5" y1="5" x2="11" y2="11" strokeLinecap="round" />
              <line x1="11" y1="5" x2="5" y2="11" strokeLinecap="round" />
            </svg>
          )}
          <span className="df-avail__status-text">
            {status === 'open' && 'Open'}
            {status === 'limited' && 'Limited'}
            {status === 'nearly-full' && 'Nearly Full'}
            {status === 'full' && 'Full'}
            {status === 'closed' && 'Closed'}
          </span>
        </span>
      </div>
      <div className="df-avail__track">
        {window.CapacityVideo ? <window.CapacityVideo status={status} /> : null}
        {/* 2026-06-03: thin glass frame over the capacity pill, matching the
            add-on onboarding-availability treatment. */}
        <span className="df-avail__frame thin-glass-frame" aria-hidden="true" />
      </div>
      <div className="df-avail__copy">{caption}</div>
    </div>
  );
}

// ── MAIN COMPONENT ─────────────────────────────────────────────────────────────────────────
// ── ROLE BREAKDOWN PANEL (Loom 34) ───────────────────────────────────
// Per-role lines in a thin-glass panel rendered in the config's LEFT column
// under Seniority, so the make-up of the combined estimate fills the space
// beside the totals card instead of living inside it. PT keeps its lines
// inside the side card, which has no fields column to share.
function DFRoleBreakdown({ lines, totalHires }) {
  if (!lines || lines.length === 0) return null;
  const n = (typeof totalHires === 'number' && totalHires > 0) ? totalHires : lines.length;
  return (
    <div className="df-role-breakdown">
      <div className="df-deposit-card__lines-head">{lines.length > 1 ? `Combined across your ${lines.length} selected roles${n > lines.length ? ' · ' + n + ' hires' : ''}` : 'Your selected role'}</div>
      {lines.map(l => (
        <div className="df-deposit-line" key={l.id}>
          <div className="df-deposit-line__main">
            <span className="df-deposit-line__name">{l.name}</span>
            <span className="df-deposit-line__meta">{l.meta}</span>
          </div>
          <div className="df-deposit-line__amt">{l.amt != null ? '~' + window.fmt(Math.round(l.amt)) + '/mo' : '—'}</div>
        </div>
      ))}
    </div>
  );
}

// ── IN-CARD CUSTOM DAYS ─────────────────────────────────────────────
// 2026-06-12 (review): the custom-days box collapses from a chevron on its
// upper right, and carries the saving badge the other day blocks already
// state in their blurbs. The percentage is computed from the role's own
// ladder (custom vs 1-day rate) with the pricing page's 15% as fallback,
// so the badge can never overstate.
function DFInCardDays({ role, days, onPatch, baseRate, ptLocMult, billing = 'recurring', rateOverride = null }) {
  const [open, setOpen] = dfUseState(true);
  const _rate = rateOverride != null ? Math.round(rateOverride) : Math.round(((role.dayRates && role.dayRates.custom) || Math.round(baseRate * (1 - 0.15))) * ptLocMult);
  const _pct = (role.dayRates && role.dayRates.d1 && role.dayRates.custom)
    ? Math.round((1 - (role.dayRates.custom / role.dayRates.d1)) * 100)
    : 15;
  return (
    <div className={`df-plan__custom df-plan__custom--incard ${open ? '' : 'df-plan__custom--closed'}`} onClick={(e) => e.stopPropagation()}>
      <div className="df-plan__custom-head">
        <label className="df-plan__custom-label" htmlFor={`custom-days-${role.id}`}>
          {billing === 'oneoff' ? 'How many days?' : 'How many days per month?'}
        </label>
        {rateOverride == null && _pct > 0 && <span className="df-plan__custom-badge">{_pct}% off the 1-day rate</span>}
        <button type="button" className="df-plan__custom-collapse" onClick={() => setOpen(o => !o)} aria-expanded={open} aria-label={open ? 'Collapse the days selector' : 'Expand the days selector'}>
          <svg viewBox="0 0 16 16" width="12" height="12" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" style={{ transform: open ? 'rotate(180deg)' : 'none', transition: 'transform 0.15s ease' }} aria-hidden="true"><polyline points="3 6 8 11 13 6"/></svg>
        </button>
      </div>
      {open && (
        <div className="df-plan__custom-input-wrap">
          <button type="button" className="df-plan__custom-step" onClick={() => onPatch({ days: Math.max(11, (days || 11) - 1) })} aria-label="Decrease days">−</button>
          <window.QtyInput id={`custom-days-${role.id}`} className="df-plan__custom-input" value={days || 11} min={11} max={22} onCommit={(v) => onPatch({ days: v })} ariaLabel="Days per month" />
          <button type="button" className="df-plan__custom-step" onClick={() => onPatch({ days: Math.min(22, (days || 11) + 1) })} aria-label="Increase days">+</button>
          <span className="df-plan__custom-suffix">{billing === 'oneoff' ? 'days' : 'days per month'}</span>
        </div>
      )}
    </div>
  );
}

// ── DEPOSIT CARD (FT shows In-House | Direct Hire tabs) ─────────────
// 2026-06-12 (review): the cost-card waiting-list badge, beside the role
// name on both tiers, with the sidebar tooltip format (body plus the amber
// no-charge line at the bottom).
function DFWaitlistBadge() {
  const pill = (
    <span className="tier__waitlist-pill tier__waitlist-pill--sm df-deposit-line__wl" tabIndex={0} role="button" aria-label="On the waiting list, hover for details">
      <span className="tier__waitlist-dot" aria-hidden="true" />
      Waiting list
    </span>
  );
  if (!window.HoverPortalTip) return pill;
  return (
    <window.HoverPortalTip wrapClassName="df-loc-tip-wrap" wrapStyle={{display:'inline-flex',verticalAlign:'middle',marginLeft:'0.35rem'}} tipClassName="dis-tip dis-tip--above" placement="above" tip={<><span className="dis-tip__body" style={{display:'block'}}>This role is currently at capacity. It stays on your quote as a waiting-list item at no charge, and we email you as soon as a spot opens, typically within 2 to 4 weeks.</span><span className="dis-tip__body" style={{display:'block',marginTop:'0.55em',color:'#b45309',fontWeight:700}}>No charge applies whilst you are on the list.</span></>}>
      {pill}
    </window.HoverPortalTip>
  );
}

function DFDepositCard({ isFT, ftMonthly, ptMonthly, selectedRoles, totalHires, deposit, showBuyout = true, lines = [], onOpenInfo, daysSlot, daysSlotRoleId, ptBilling = 'recurring', onSetPtBilling, onFocusRole }) {
  const [tab, setTab] = dfUseState('costs');
  // 2026-06-08: deposit now scales with total hires (qty per role), not just
  // the number of distinct roles.
  const n = (typeof totalHires === 'number' && totalHires > 0) ? totalHires : selectedRoles.length;
  const _isWlDep = !!(window.__lastBuildPageState && window.__lastBuildPageState.intentId === 'agency-whitelabel');
  const _depWl = _isWlDep ? Math.round(deposit * 0.6) : deposit;
  if (!isFT) {
    /* 2026-06-11 (Nicole): rows and total in one box, per-row day maths in
       tooltips, the box never grows past the left column (rows scroll). */
    return (
      <div className="df-deposit-card df-deposit-card--merged">
        {/* 2026-06-12 (Loom 41 2:37, Nicole's Option A): one-off vs recurring
            billing pill in the Pay-upfront pattern. Recurring (default) keeps
            the commitment-ladder pricing, the recurring discount. One-off
            books the selected days once at the full day rate. */}
        {onSetPtBilling && (
          <div className="df-billing-row">
            <button
              type="button"
              className={`svc__upfront-toggle df-billing-toggle ${ptBilling !== 'oneoff' ? 'is-on' : ''}`}
              role="switch"
              aria-checked={ptBilling !== 'oneoff'}
              aria-label="Recurring booking, save 20% on a 3-month minimum commitment"
              onClick={(e) => { e.stopPropagation(); onSetPtBilling(ptBilling === 'oneoff' ? 'recurring' : 'oneoff'); }}
            >
              <span className="svc__upfront-knob" aria-hidden="true" />
              <span className="svc__upfront-text">Recurring</span>
              <span className="svc__upfront-save">Save 20%</span>
            </button>
            {window.HoverPortalTip && (
              <window.HoverPortalTip wrapClassName="df-loc-tip-wrap" wrapStyle={{display:'inline-flex',verticalAlign:'middle'}} tipClassName="dis-tip dis-tip--above" placement="above" tip={"Recurring bookings repeat monthly on a 3-month minimum commitment and save 20% off the day rate. Switch off to book your selected days as a one-off block at the full day rate, billed once."}>
                <window.InfoIcon title="About recurring and one-off pricing" onClick={(e) => e.stopPropagation()} />
              </window.HoverPortalTip>
            )}
          </div>
        )}
        {onSetPtBilling && (
          <div className={`svc__cat df-commit-eyebrow df-pt-commit-eyebrow${ptBilling === 'oneoff' ? ' df-pt-commit-eyebrow--off' : ''}`} aria-hidden={ptBilling === 'oneoff' ? 'true' : undefined}>
            3-month minimum commitment
            {window.HoverPortalTip && (
              <window.HoverPortalTip wrapClassName="svc__commit-eyebrow-tip" tipClassName="dis-tip dis-tip--above" placement="above" tip={<><span className="dis-tip__body" style={{display:'block'}}>Your recurring booking runs as a monthly rolling contract with a 3-month minimum commitment to start.</span><span className="dis-tip__body" style={{display:'block',marginTop:'0.55em'}}>After the minimum, you can cancel any time with 28 days’ notice.</span></>}>
                <window.InfoIcon className="svc__commit-eyebrow-info" />
              </window.HoverPortalTip>
            )}
          </div>
        )}
        {lines.length > 0 && (
          <div className="df-deposit-card__lines-head">{lines.length > 1 ? `Combined across your ${lines.length} selected roles` : 'Your selected role'}</div>
        )}
        {lines.length > 0 && (
          <div className="df-deposit-card__lines df-deposit-card__lines--scroll">
            {lines.map(l => (
              <React.Fragment key={l.id}>
              <div className="df-deposit-line">
                {/* 2026-06-12 (Loom 45, Nicole): explicit blue link opens the
                    role overlay from the cost card, clearer than the clickable
                    tooltip icon whose affordance only shows on hover. */}
                <span className="df-deposit-line__namewrap">
                  <span className="df-deposit-line__name"><button type="button" className="df-deposit-line__name-btn" onClick={(e) => { e.stopPropagation(); if (onFocusRole) onFocusRole(l.id); }} title="Open this role's settings">{l.name}</button>{l.wl ? <DFWaitlistBadge /> : null}{l.id === 'pt-custom' && window.HoverPortalTip ? (
                    <window.HoverPortalTip wrapClassName="df-loc-tip-wrap" wrapStyle={{display:'inline-flex',verticalAlign:'middle',marginLeft:'0.3rem'}} tipClassName="dis-tip dis-tip--above" placement="above" tip={"Estimated from the day rate you entered × the days you booked. It is your budget for the role rather than a final price, we confirm the rate after we shortlist candidates to your brief."}>
                      <window.InfoIcon title="About this estimate" onClick={(e) => e.stopPropagation()} />
                    </window.HoverPortalTip>
                  ) : null}</span>
                  {onOpenInfo && (window.DEDICATED_ROLE_OVERLAYS || {})[l.id] ? (
                    <button type="button" className="df-deposit-line__more" onClick={(e) => { e.stopPropagation(); onOpenInfo(l.id); }}><span className="df-deposit-line__more-text">See what this role can do for you</span> <span className="df-deposit-line__more-arrow" aria-hidden="true">›</span></button>
                  ) : null}
                </span>
                <span className="df-deposit-line__amtwrap">
                  <span className="df-deposit-line__amt">{l.amt != null ? '~' + window.fmt(Math.round(l.amt)) + (ptBilling === 'oneoff' ? '' : '/mo') : '—'}</span>
                  <span className="df-deposit-line__days">{l.wl ? '' : (l.days > 0 ? (ptBilling === 'oneoff' ? (l.days + (l.days === 1 ? ' day' : ' days') + ' in total') : (l.days + (l.days === 1 ? ' day per month' : ' days per month'))) : (ptBilling === 'oneoff' ? 'Choose days' : 'Choose days per month'))}</span>
                  
                </span>
              </div>
              {/* 2026-06-12 (review): the custom-days stepper docks directly
                  under the role it configures, so there is no doubt which
                  role's days are being set. */}
              {daysSlot && daysSlotRoleId === l.id ? daysSlot : null}
              </React.Fragment>
            ))}
          </div>
        )}
        <div className="df-deposit-card__totals df-deposit-card__totals--solo df-deposit-card__totals--pinned">
          <div className="df-deposit-total df-deposit-total--inline">
            <div className="df-deposit-card__label">{ptBilling === 'oneoff' ? 'Estimated one-off total' : 'Estimated monthly total'}{window.HoverPortalTip && (<window.HoverPortalTip wrapClassName="df-loc-tip-wrap" wrapStyle={{marginLeft:'0.4rem',display:'inline-flex',verticalAlign:'middle'}} tipClassName="dis-tip dis-tip--above" placement="above" tip={<><span className="dis-tip__body" style={{display:'block'}}>{ptBilling === 'oneoff' ? "Each role's day rate × the days you booked, billed once at the full day rate with no commitment discount, with any partner discount already applied, so this figure matches your summary." : "Each role's day rate × the days a month you booked, with the 20% recurring saving and any partner discount already applied, so this figure matches your summary. Recurring bookings run on a 3-month minimum commitment."}</span><span className="dis-tip__body" style={{display:'block',marginTop:'0.55em'}}>This is an estimate. Final pricing is confirmed on your scoping call.</span></>}><window.InfoIcon title="About this estimate" onClick={(e) => e.stopPropagation()} /></window.HoverPortalTip>)}</div>
            <div className="df-deposit-card__amt df-deposit-total__amt" style={_isWlDep ? { color: 'var(--gg-orange-deep, #C77800)' } : undefined}>{ptMonthly > 0 ? <DFAnimatedMoney value={ptMonthly} /> : '—'}</div>
          </div>
          <div className="df-deposit-total df-deposit-total--deposit df-deposit-total--inline">
            <div className="df-deposit-card__label df-deposit-card__label--deposit">Starting deposit{n > 1 ? ` × ${n} hires` : ''}{window.HoverPortalTip && (<window.HoverPortalTip wrapClassName="df-loc-tip-wrap" wrapStyle={{marginLeft:'0.4rem',display:'inline-flex',verticalAlign:'middle'}} tipClassName="dis-tip dis-tip--above dis-tip--compact" placement="above" tip={<span style={{display:'block',textAlign:'left'}}>Each role carries a £{deposit} deposit, due today and fully refundable, and it is included in your total.{_isWlDep ? ` As a white-label partner you get it at your 40% reseller rate, so you pay £${_depWl} and charge your client the £${deposit}.` : ''}</span>}><window.InfoIcon title="About the deposit" onClick={(e) => e.stopPropagation()} /></window.HoverPortalTip>)}</div>
            <div className="df-deposit-card__amt df-deposit-card__amt--deposit df-deposit-total__amt" style={_isWlDep ? { color: 'var(--gg-orange-deep, #C77800)' } : undefined}><DFAnimatedMoney value={_depWl * n} />{_isWlDep ? <span style={{fontWeight:400, fontStyle:'italic', fontSize:'0.72em', marginLeft:'4px', color:'var(--gg-blue, #002abf)'}}>(RRP £{(deposit * n).toLocaleString('en-GB')})</span> : null}</div>
          </div>
        </div>
      </div>
    );
  }
  // 2026-06-09: Talent buyout (Direct hire) now applies to all locations,
  // including the Philippines, at industry-standard country rates.
  const _showBuyout = showBuyout !== false;
  const _activeTab = _showBuyout ? tab : 'costs';
  return (
    <div className="df-deposit-card">
      {_showBuyout && (
        <div className="df-deposit-tabs" role="tablist">
          <button type="button" role="tab" aria-selected={_activeTab === 'costs'} className={`df-deposit-tab ${_activeTab === 'costs' ? 'df-deposit-tab--on' : ''}`} onClick={() => setTab('costs')}>
            In-House
            {window.HoverPortalTip && (
              <window.HoverPortalTip wrapClassName="df-deposit-tab-tip" wrapStyle={{display:'inline-flex',verticalAlign:'middle'}} tipClassName="dis-tip dis-tip--above" placement="above" tip={"What you pay GoGorilla.com whilst we employ the specialist for you. We handle payroll, HR, and compliance, and you direct the day-to-day work."}>
                <span className="info-tip-icon" role="img" aria-label="About In-House pricing"><svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true"><circle cx="12" cy="12" r="10"/><line x1="12" y1="16" x2="12" y2="12"/><line x1="12" y1="8" x2="12.01" y2="8"/></svg></span>
              </window.HoverPortalTip>
            )}
          </button>
          <button type="button" role="tab" aria-selected={_activeTab === 'buyout'} className={`df-deposit-tab ${_activeTab === 'buyout' ? 'df-deposit-tab--on' : ''}`} onClick={() => setTab('buyout')}>
            Direct Hire
            {window.HoverPortalTip && (
              <window.HoverPortalTip wrapClassName="df-deposit-tab-tip" wrapStyle={{display:'inline-flex',verticalAlign:'middle'}} tipClassName="dis-tip dis-tip--above" placement="above" tip={"The buyout pathway if you later want to employ the specialist directly yourself, available on United Kingdom and South Africa placements. The fee is an industry-standard share of first-year salary, 15% in the United Kingdom and 20% in South Africa, and from then on you take on payroll and employment directly."}>
                <span className="info-tip-icon" role="img" aria-label="About Direct Hire"><svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true"><circle cx="12" cy="12" r="10"/><line x1="12" y1="16" x2="12" y2="12"/><line x1="12" y1="8" x2="12.01" y2="8"/></svg></span>
              </window.HoverPortalTip>
            )}
          </button>
        </div>
      )}
      {/* 2026-06-10 (Nicole): both tab panes stay mounted in a grid stack so
          the card keeps one height whichever tab is open (with 1 role the
          In-House pane is shorter than Direct Hire). */}
      <div className="df-deposit-panes">
        <div className={`df-deposit-pane df-deposit-pane--costs ${_activeTab === 'costs' ? 'is-on' : ''}`} aria-hidden={_activeTab !== 'costs'}>
        {lines.length > 0 && (
          <div className="df-deposit-card__lines-head">{lines.length > 1 ? `Combined across your ${lines.length} selected roles${n > lines.length ? ' · ' + n + ' hires' : ''}` : 'Your selected role'}</div>
        )}
        {lines.length > 0 && (
          <div className="df-deposit-card__lines df-deposit-card__lines--scroll">
            {lines.map(l => (
              <div className="df-deposit-line" key={l.id}>
                {/* 2026-06-12 (Loom 45, Nicole): explicit blue link opens the
                    role overlay from the cost card, clearer than the clickable
                    tooltip icon whose affordance only shows on hover. */}
                <span className="df-deposit-line__namewrap">
                  <span className="df-deposit-line__name"><button type="button" className="df-deposit-line__name-btn" onClick={(e) => { e.stopPropagation(); if (onFocusRole) onFocusRole(l.id); }} title="Open this role's settings">{l.name}</button>{l.wl ? <DFWaitlistBadge /> : null}{l.id === 'ft-custom' && window.HoverPortalTip ? (
                    <window.HoverPortalTip wrapClassName="df-loc-tip-wrap" wrapStyle={{display:'inline-flex',verticalAlign:'middle',marginLeft:'0.3rem'}} tipClassName="dis-tip dis-tip--above" placement="above" tip={"Estimated from the hourly rate you entered, multiplied by 173 working hours a month. It is your budget for the role rather than a final price, we confirm the monthly fee after we shortlist candidates to your brief."}>
                      <window.InfoIcon title="About this estimate" onClick={(e) => e.stopPropagation()} />
                    </window.HoverPortalTip>
                  ) : null}</span>
                  {onOpenInfo && (window.DEDICATED_ROLE_OVERLAYS || {})[l.id] ? (
                    <button type="button" className="df-deposit-line__more" onClick={(e) => { e.stopPropagation(); onOpenInfo(l.id); }}><span className="df-deposit-line__more-text">See what this role can do for you</span> <span className="df-deposit-line__more-arrow" aria-hidden="true">›</span></button>
                  ) : null}
                </span>
                <span className="df-deposit-line__amtwrap">
                  <span className="df-deposit-line__amt">{l.amt != null ? '~' + window.fmt(Math.round(l.amt)) + '/mo' : '—'}</span>
                  {/* 2026-06-12 (review): the estimate tooltip moved beside the
                      Custom-role name and the waiting-list badge beside the
                      role name, both live in the name column now. */}
                </span>
              </div>
            ))}
          </div>
        )}
        <div className="df-deposit-card__totals df-deposit-card__totals--solo df-deposit-card__totals--pinned">
            <div className="df-deposit-total df-deposit-total--inline">
              <div className="df-deposit-card__label">Estimated monthly total{window.HoverPortalTip && (<window.HoverPortalTip wrapClassName="df-loc-tip-wrap" wrapStyle={{marginLeft:'0.4rem',display:'inline-flex',verticalAlign:'middle'}} tipClassName="dis-tip dis-tip--above" tip={<><span className="dis-tip__body" style={{display:'block'}}>Your estimate is each specialist's hourly rate × about 173 hours a month, which covers annual leave and local hiring and compliance costs. Until we shortlist, it shows the floor rate for each role.</span><span className="dis-tip__body" style={{display:'block',marginTop:'0.55em'}}>The final monthly fee is confirmed after the candidate shortlist and billed on placement, and your deposit is fully refundable if no placement is made.</span></>} placement="above"><window.InfoIcon title="About this estimate" onClick={(e) => e.stopPropagation()} /></window.HoverPortalTip>)}</div>
              <div className="df-deposit-card__amt df-deposit-total__amt" style={_isWlDep ? { color: 'var(--gg-orange-deep, #C77800)' } : undefined}>{ftMonthly > 0 ? <>~<DFAnimatedMoney value={ftMonthly} /></> : 'Custom'}</div>
              </div>
            <div className="df-deposit-total df-deposit-total--deposit df-deposit-total--inline">
              <div className="df-deposit-card__label df-deposit-card__label--deposit">Starting deposit{n > 1 ? ` × ${n} hires` : ''}{window.HoverPortalTip && (<window.HoverPortalTip wrapClassName="df-loc-tip-wrap" wrapStyle={{marginLeft:'0.4rem',display:'inline-flex',verticalAlign:'middle'}} tipClassName="dis-tip dis-tip--above dis-tip--compact" placement="above" tip={<span style={{display:'block',textAlign:'left'}}>Each role carries a £{deposit} deposit, due today and fully refundable, and it is included in your total.{_isWlDep ? ` As a white-label partner you get it at your 40% reseller rate, so you pay £${_depWl} and charge your client the £${deposit}, alongside the 10% recurring commission once the placement proceeds.` : ''}</span>}><window.InfoIcon title="About the deposit" onClick={(e) => e.stopPropagation()} /></window.HoverPortalTip>)}</div>
              <div className="df-deposit-card__amt df-deposit-card__amt--deposit df-deposit-total__amt" style={_isWlDep ? { color: 'var(--gg-orange-deep, #C77800)' } : undefined}><DFAnimatedMoney value={_depWl * n} />{_isWlDep ? <span style={{fontWeight:400, fontStyle:'italic', fontSize:'0.72em', marginLeft:'4px', color:'var(--gg-blue, #002abf)'}}>(RRP £{(deposit * n).toLocaleString('en-GB')})</span> : null}</div>
              </div>
        </div>
        </div>
        <div className={`df-deposit-pane ${_activeTab === 'buyout' ? 'is-on' : ''}`} aria-hidden={_activeTab !== 'buyout'}>
        <div className="df-deposit-buyout">
          <div className="df-deposit-buyout__lede">Hire the specialist onto your own payroll whenever you are ready, on United Kingdom and South Africa placements.{window.HoverPortalTip && (<window.HoverPortalTip wrapClassName="df-loc-tip-wrap" wrapStyle={{marginLeft:'0.4rem',display:'inline-flex',verticalAlign:'middle'}} tipClassName="dis-tip dis-tip--above" placement="above" tip={"The fee is industry standard and based on the specialist's annual salary. From that point, you take on all local payroll and employment."}><window.InfoIcon title="About the buyout" onClick={(e) => e.stopPropagation()} /></window.HoverPortalTip>)}</div>
          <ul className="df-deposit-buyout__tiers">
            <li><span>South Africa</span><span>20% of annual salary</span></li>
            <li><span>United Kingdom</span><span>15% of annual salary</span></li>
          </ul>
          <div className="df-deposit-card__sub">Most clients stay on the managed model. Agreeing the pathway upfront means no surprises.</div>
        </div>
        </div>
      </div>
    </div>
  );
}

function DedicatedFlow({ service, selection, onSelect, onTier, onToggleRole, onSetRoleConfig, onSetStartWindow, onSetPtBilling }) {
  const active = !!selection;
  const tier = selection?.tier || service.tiers?.[0]?.id || 'parttime';
  const isFT = tier === 'fulltime';
  const selectedRoles = selection?.roles || [];
  const cfgs = selection?.roleConfigs || {};
  // 2026-06-12 (Loom 41 2:37, Nicole's Option A): PT billing mode, recurring
  // by default. One-off books the selected days once at the full day rate.
  const ptBilling = selection?.ptBilling === 'oneoff' ? 'oneoff' : 'recurring';
  const roles = dfRolesForTier(service, tier);

  const [focusedRoleId, setFocusedRoleId] = dfUseState(null);
  // Role overlay state hoisted here (GorillaMatrix pattern): the modal must
  // never mount inside the tooltip wrapper or the card, or portal events
  // bubble back up and re-show the tip over the dialog.
  const [infoRoleId, setInfoRoleId] = dfUseState(null);
  dfUseEffect(() => {
    if (selectedRoles.length === 0) {
      if (focusedRoleId !== null) setFocusedRoleId(null);
      return;
    }
    if (!focusedRoleId || !selectedRoles.includes(focusedRoleId)) {
      setFocusedRoleId(selectedRoles[selectedRoles.length - 1]);
    }
  }, [selectedRoles.join('|')]);

  const handleTier = (tid) => {
    if (active && tier === tid) { onSelect(false); return; }
    if (!active) onSelect(true);
    onTier(tid);
  };

  const handleToggleRole = (rid) => {
    const isRemoving = active && selectedRoles.includes(rid);
    const isLastRole = isRemoving && selectedRoles.length === 1;
    // Activating dedicated-ft via a role-tile click: SET_SERVICE auto-seeds
    // the qualifier-driven default roles (founders-talent §5). If the clicked
    // role is among those defaults, TOGGLE_ROLE here would immediately remove
    // it. Detect that case and skip the toggle, the seed already includes it.
    const isFirstActivation = !active;
    let skipToggleAfterSeed = false;
    if (isFirstActivation && service?.id === 'dedicated-ft' && tier === 'fulltime') {
      const defaults = window.getDrFtDefaultRoles
        ? window.getDrFtDefaultRoles(window.__lastBuildPageState)
        : [];
      if (defaults.includes(rid)) skipToggleAfterSeed = true;
    }
    if (isFirstActivation && service?.id === 'dedicated-pt' && tier === 'parttime') {
      const defaults = window.getDrPtDefaultRoles
        ? window.getDrPtDefaultRoles(window.__lastBuildPageState)
        : [];
      if (defaults.includes(rid)) skipToggleAfterSeed = true;
    }
    if (!active) onSelect(true);
    if (isLastRole) { onSelect(false); return; }
    if (!skipToggleAfterSeed) onToggleRole(rid);
    if (!isRemoving) {
      setFocusedRoleId(rid);
      const role = dfRoleById(service, tier, rid);
      if (role) {
        if (tier === 'parttime' && !cfgs[rid]?.days) {
          onSetRoleConfig(rid, { days: 5 });
        } else if (tier === 'fulltime') {
          // Set the recommended location as the default when a FT role is first selected.
          // ROLE_RECS and FT_LOCATIONS are local to this file, no window lookup needed.
          const recEntry = ROLE_RECS[rid];
          const recLoc = recEntry ? FT_LOCATIONS.find(l => l.cc === recEntry.cc) : null;
          const recLocId = recLoc ? recLoc.id : 'philippines';
          onSetRoleConfig(rid, { location: recLocId, seniority: 'mid', tasks: '' });
        }
      }
    }
  };

  const focusedRole = focusedRoleId ? dfRoleById(service, tier, focusedRoleId) : null;
  const focusedCfg = focusedRoleId ? (cfgs[focusedRoleId] || {}) : {};

  let ptMonthly = 0;
  // 2026-06-10 (Loom 34): per-role breakdown lines for the PT cost card, so
  // the estimate clearly combines every selected role, not just the open tab.
  const ptLines = [];
  if (!isFT) {
    selectedRoles.forEach(rid => {
      const role = dfRoleById(service, tier, rid);
      const days = cfgs[rid]?.days || 0;
      if (!role || !role.price) return;
      let dayRate;
      if (rid === 'pt-custom') {
        // 2026-06-12 (review): the custom role prices from the typed day
        // rate, flat across blocks and locations.
        const _tr = parseFloat(cfgs[rid]?.customRate) || 0;
        dayRate = _tr > 0 ? Math.max(150, Math.round(_tr)) : 0;
      } else if (role.dayRates) {
        if (days === 1) dayRate = role.dayRates.d1;
        else if (days === 5) dayRate = role.dayRates.d5;
        else if (days === 10) dayRate = role.dayRates.d10;
        else if (days >= 11) dayRate = role.dayRates.custom;
        else dayRate = role.dayRates.d1;
      } else {
        let discount = 0;
        if (days === 5) discount = 0.10;
        else if (days === 10) discount = 0.12;
        else if (days >= 11) discount = 0.15;
        dayRate = Math.round(role.price * (1 - discount));
      }
      // 2026-06-10: scale the PT day rate by the selected office location.
      if (rid !== 'pt-custom') {
        const _ptLocMult = ((window.DEDICATED_FT_LOCATIONS || []).find(l => l.id === (cfgs[rid]?.location || 'philippines')) || {}).multLo || 1;
        dayRate = Math.round(dayRate * _ptLocMult);
      }
      // 2026-06-03: include the per-role commitment discount so the
      // "Estimated monthly" matches the cart line (which now applies it).
      // 2026-06-12 (Nicole): the recurring saving is a flat 20% on a stated
      // 3-month minimum, the silent 12-month ladder default is retired.
      const _ptSave = ptBilling === 'oneoff' ? 0 : 20;
      // 2026-06-12 (review): the estimate carries the same partner discount
      // as the sidebar line, so the two always agree.
      const _ptPartnerMult = (window.getAgencyMultiplier && window.__lastBuildPageState) ? window.getAgencyMultiplier(window.__lastBuildPageState, service.id) : 1;
      const _amt = dayRate * days * (1 - _ptSave / 100) * _ptPartnerMult;
      ptMonthly += role.waitlist ? 0 : _amt;
      // 2026-06-12 (Loom 41 0:30): the PT custom role carries its typed name.
      const _ptTyped = (rid === 'pt-custom' && String(cfgs[rid]?.customName || '').trim()) || '';
      const _ptNm = _ptTyped ? `${_ptTyped} (Custom)` : role.name;
      ptLines.push({
        id: rid,
        name: _ptNm,
        wl: !!role.waitlist,
        days,
        meta: role.waitlist
          ? 'Waiting list · no charge until a spot opens'
          : (days > 0
            ? (ptBilling === 'oneoff'
                ? `${days} ${days === 1 ? 'day' : 'days'} × £${dayRate}/day · one-off`
                : `${days} ${days === 1 ? 'day' : 'days'}/mo × £${dayRate}/day · recurring −20%`)
            : (rid === 'pt-custom' && !(dayRate > 0) ? 'Type a day rate to estimate' : (ptBilling === 'oneoff' ? 'Choose days to estimate' : 'Choose days per month to estimate'))),
        amt: role.waitlist ? null : (days > 0 && dayRate > 0 ? _amt : null),
      });
    });
  }

  // 2026-06-03: indicative FT monthly estimate (rate x 173 hours a month, Loom 34),
  // summed across configured roles, so the card shows a number instead of just "Custom".
  // 2026-06-10 (Loom 34): also builds per-role breakdown lines for the cost
  // card, so the estimate clearly combines every selected role, not just the
  // open role tab.
  let ftMonthly = 0;
  const ftLines = [];
  if (isFT) {
    selectedRoles.forEach(rid => {
      const c = cfgs[rid] || {};
      // 2026-06-09: custom role — rate x 173 hours a month, no commitment
      // discount, quantity fixed at 1.
      if (rid === 'ft-custom') {
        // 2026-06-12 (Nicole): £6/hour floor on the budget, and the chosen
        // location rides the meta line (Philippines-only assumption retired).
        const _crRaw = parseFloat(c.customRate) || 0;
        const _cr = _crRaw > 0 ? Math.max(6, _crRaw) : 0;
        const _nmTyped = String(c.customName || '').trim();
        const _nm = _nmTyped ? `${_nmTyped} (Custom)` : 'Custom role';
        const _locLbl = ((FT_LOCATIONS.find(l => l.id === c.location) || {}).label) || 'Philippines';
        if (_cr > 0) {
          ftMonthly += _cr * 173;
          ftLines.push({ id: rid, name: _nm, meta: '£' + _cr + '/hour · ' + _locLbl, amt: _cr * 173 });
        } else {
          ftLines.push({ id: rid, name: _nm, meta: 'Set an hourly rate to estimate', amt: null });
        }
        return;
      }
      const role = dfRoleById(service, tier, rid);
      if (!role) return;
      const _ftQty = Math.max(1, Math.min(99, c.qty || 1));
      const _nm = role.name + (_ftQty > 1 ? ' × ' + _ftQty : '');
      // 2026-06-12 (review): waitlisted FT roles read Waiting list, nothing
      // is charged whilst they wait, matching the Part-Time treatment.
      if (role.waitlist) {
        ftLines.push({ id: rid, name: _nm, wl: true, meta: 'Waiting list · no charge until a spot opens', amt: null });
        return;
      }
      const rate = (typeof role.hourlyFrom === 'number' && c.location && c.seniority && window.dfSeniorityRate)
        ? window.dfSeniorityRate(role, c.location, c.seniority) : null;
      if (rate == null) {
        ftLines.push({ id: rid, name: _nm, meta: 'Choose location and seniority to estimate', amt: null });
        return;
      }
      const _loc = FT_LOCATIONS.find(l => l.id === c.location);
      const _sen = FT_SENIORITY.find(s => s.id === c.seniority);
      // 2026-06-10: flat 3-month minimum commitment - no commit discount.
      const _ftm = 1.0;
      const _amt = Math.round(Math.max(6, rate * _ftm) * 173) * _ftQty;
      ftMonthly += _amt;
      ftLines.push({ id: rid, name: _nm, meta: (_loc ? _loc.label : '') + ' · ' + (_sen ? _sen.label : ''), amt: _amt });
    });
  }

  // 2026-06-10 (Loom 34 0:27): the Direct Hire (talent buyout) tab only
  // exists for United Kingdom and South Africa placements. Philippines-only
  // selections see no buyout pathway.
  const buyoutEligible = isFT && selectedRoles.some(rid => {
    const _l = (cfgs[rid] || {}).location;
    return _l === 'uk' || _l === 'south-africa';
  });

  // Preserve the original order of role cards regardless of selection state.
  // Previously we hoisted selected roles to the front, but that mid-flow
  // reflow confused users, selecting a role made the rest of the list jump.
  const sortedRoles = roles;

  // §3.3, 2-column trust grid. Verbatim copy from the talent spec. Only
  // surfaces on FT (PT has a simpler single-line trust line).
  const FT_TRUST_LEFT = [
    { label: '90-Day Performance and Replacement Guarantee',                  tip: 'You are covered by two guarantees. If a placed specialist is not the right fit within their first 90 days, we replace them at no charge. We also agree the performance bar with you on the kick-off call and hold ourselves to it, so both sides know what a good fit looks like before day one.' },
    { label: 'Algorithmic Skill Matching & Practical Assessments',           tip: 'Every candidate is scored against your brief through a structured skills test, then a 30-minute video panel with our talent team. You receive two or three shortlisted candidates with their scores attached.' },
    { label: 'GorillaMatrix Performance Tracking',                           tip: 'Every placed specialist is tracked in GorillaMatrix for output, quality, and brief completion. You get the dashboard, and we run performance reviews at months 1, 3, 6, and 12.' },
    { label: 'Dedicated Account Management',                                 tip: 'A UK-based account manager owns the relationship, with weekly check-ins, escalation routing, performance reviews, and brief refinement.' },
    { label: 'GorillaPerks Investor Portal & Bundle Discounts',              tip: 'Every active talent engagement unlocks the GorillaPerks portal, with investor-network access, multi-service bundle discounts, partner tooling discounts, and warm founder and investor introductions.' },
  ];
  const FT_TRUST_RIGHT = [
    { label: 'Access to Global Talent',                                      tip: 'We recruit from over 130 countries, with a particular focus on the UK, South Africa, and the Philippines. We can advise which country is likely to give you the best talent for each role whilst keeping you capital efficient.' },
    { label: 'Full HR & Payroll Administration',                             tip: 'We handle hiring, contracts, payroll, leave, performance reviews, and offboarding. You manage the work, and we manage the employment relationship.' },
    { label: 'International Compliance Handled',                             tip: 'Local employment law, tax, employer social costs, and data-protection rules, including GDPR and its local equivalents, are handled in the specialist home country by our local entities or trusted employer-of-record partners.' },
    { label: 'Ongoing Training & Development',                               tip: 'Our specialists receive ongoing training and development, so they stay at the forefront of their fields.' },
    { label: 'Talent Buyout Option',                                         tip: 'Take the specialist on as your own direct employee whenever you are ready. Available on United Kingdom and South Africa placements, at an industry-standard 20% of annual salary in South Africa and 15% in the UK. You take on all local payroll and employment from that point, so most clients stay on the managed model.' },
  ];

  return (
    <div className="df">
      <DFTimeToggle service={service} tier={selection?.tier} onTier={handleTier} hasSelection={active} />

      {isFT && (
        <div className="df-trust-grid" role="list" aria-label="What you get with embedded full-time talent">
          <div className="df-trust-grid__col">
            <div className="df-trust-grid__title">Talent Acquisition</div>
            <ul className="df-trust-grid__list">
              {FT_TRUST_LEFT.map((p, i) => (
                <li key={'l'+i} className="df-trust-grid__item" role="listitem">
                  <svg className="df-trust-grid__check" viewBox="0 0 16 16" width="13" height="13" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
                    <polyline points="3 8.5 6.5 12 13 4.5"/>
                  </svg>
                  <span className="df-trust-grid__label">{p.label}</span>
                  {window.HoverPortalTip ? (
                    <window.HoverPortalTip
                      wrapClassName="df-trust-grid__info-wrap"
                      tipClassName="dis-tip dis-tip--above"
                      placement="above"
                      tip={p.tip}
                    >
                      <button type="button" className="df-trust-grid__info" aria-label={`About ${p.label}`} tabIndex={0}>
                        <svg viewBox="0 0 16 16" width="11" height="11" fill="none" stroke="currentColor" strokeWidth="1.6" aria-hidden="true">
                          <circle cx="8" cy="8" r="6.5"/>
                          <line x1="8" y1="11" x2="8" y2="7" strokeLinecap="round"/>
                          <circle cx="8" cy="5" r="0.5" fill="currentColor"/>
                        </svg>
                      </button>
                    </window.HoverPortalTip>
                  ) : (
                    <span className="df-trust-grid__info" title={p.tip} aria-label={`About ${p.label}`}>i</span>
                  )}
                </li>
              ))}
            </ul>
          </div>
          <div className="df-trust-grid__col">
            <div className="df-trust-grid__title">Talent Operations</div>
            <ul className="df-trust-grid__list">
              {FT_TRUST_RIGHT.map((p, i) => (
                <li key={'r'+i} className="df-trust-grid__item" role="listitem">
                  <svg className="df-trust-grid__check" viewBox="0 0 16 16" width="13" height="13" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
                    <polyline points="3 8.5 6.5 12 13 4.5"/>
                  </svg>
                  <span className="df-trust-grid__label">{p.label}</span>
                  {window.HoverPortalTip ? (
                    <window.HoverPortalTip
                      wrapClassName="df-trust-grid__info-wrap"
                      tipClassName="dis-tip dis-tip--above"
                      placement="above"
                      tip={p.tip}
                    >
                      <button type="button" className="df-trust-grid__info" aria-label={`About ${p.label}`} tabIndex={0}>
                        <svg viewBox="0 0 16 16" width="11" height="11" fill="none" stroke="currentColor" strokeWidth="1.6" aria-hidden="true">
                          <circle cx="8" cy="8" r="6.5"/>
                          <line x1="8" y1="11" x2="8" y2="7" strokeLinecap="round"/>
                          <circle cx="8" cy="5" r="0.5" fill="currentColor"/>
                        </svg>
                      </button>
                    </window.HoverPortalTip>
                  ) : (
                    <span className="df-trust-grid__info" title={p.tip} aria-label={`About ${p.label}`}>i</span>
                  )}
                </li>
              ))}
            </ul>
          </div>
        </div>
      )}

      {/* 2026-06-06: onboarding availability indicator removed on Full-Time
          per Alexander; still shown on Part-Time. When hidden, drop the
          --stacked modifier so the Roles header keeps its normal spacing. */}
      <div className="df-section__head">
        <div className="df-section__head-left">
          <div className="df-section__title">
            Roles
            {window.HoverPortalTip && (
              <window.HoverPortalTip
                wrapClassName="df-loc-tip-wrap"
                wrapStyle={{marginLeft: 0, display: 'inline-flex', verticalAlign: 'middle'}}
                tipClassName="dis-tip dis-tip--above"
                placement="above"
                tip={tier === 'fulltime'
                  ? 'These are indicative starting hourly rates. The final monthly fee is confirmed after we shortlist candidates to your brief, and rates vary by location and seniority.'
                  : 'These are day rates, with a better rate the more days a month you book. Final pricing is confirmed on your scoping call.'}
              >
                <span className="df-section__info" tabIndex={-1}>
                  <svg viewBox="0 0 16 16" width="13" height="13" fill="none" stroke="currentColor" strokeWidth="1.6">
                    <circle cx="8" cy="8" r="6.5"/>
                    <path d="M8 11V7M8 5h0" strokeLinecap="round"/>
                  </svg>
                </span>
              </window.HoverPortalTip>
            )}
          </div>
        </div>
      </div>

      {/* §13, soft "too early for FT" warning. Surfaces only for
          pre-revenue founders with sub-£1k ACV. Reads the global
          BuildPage state since DedicatedFlow doesn't receive qualifier
          as a prop. Pure render, no state changes from the warning. */}
      {tier === 'fulltime' && window.isDrFtTooEarlyWarning && window.isDrFtTooEarlyWarning(window.__lastBuildPageState) && (
        <div className="df-too-early-warning" role="note">
          <svg className="df-too-early-warning__icon" viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
            <path d="M10.29 3.86 1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"/>
            <line x1="12" y1="9" x2="12" y2="13"/>
            <circle cx="12" cy="17" r="0.9" fill="currentColor" stroke="none"/>
          </svg>
          <span>
            Full-time embeds start around <strong>£3,000/mo</strong>. At your stage, a Part-Time block or a managed retainer is often a better fit. You can still proceed. We will calibrate scope at kick-off.
          </span>
        </div>
      )}

      <div className="df-roles-wrap">
        {/* 2026-06-10: FT grid gets a fixed 5-per-row desktop layout */}
        <div className={`df-role-grid ${tier === 'fulltime' ? 'df-role-grid--ft' : ''}`}>
          {sortedRoles.map(role => {
            const on = selectedRoles.includes(role.id);
            return (
              <DFRoleCard
                key={role.id}
                role={role}
                on={on}
                isFocused={role.id === focusedRoleId}
                onToggle={() => handleToggleRole(role.id)}
                onFocus={() => setFocusedRoleId(role.id)}
                isFT={isFT}
                rec={ROLE_RECS[role.id] || null}
                onOpenInfo={setInfoRoleId}
              />
            );
          })}
          {/* 2026-06-09: "Custom role" card (FT). Lets the client define a
              bespoke role; its config form (name / rate / brief) opens below.
              2026-06-12 (Loom 41 0:30): PT gains the same card as pt-custom,
              day-rate priced, configured through the standard day-rate tiles. */}
          {(() => {
            const _cid = isFT ? 'ft-custom' : 'pt-custom';
            const _cc = cfgs[_cid] || {};
            const on = selectedRoles.includes(_cid);
            const _name = String(_cc.customName || '').trim();
            return (
              <div
                className={`df-role-card df-role-card--custom ${on ? 'df-role-card--on' : 'df-role-card--dim'} ${focusedRoleId === _cid ? 'df-role-card--focused' : ''}`}
                role="checkbox"
                aria-checked={on}
                tabIndex={0}
                onClick={() => handleToggleRole(_cid)}
                onKeyDown={e => { if (e.key === ' ' || e.key === 'Enter') { e.preventDefault(); handleToggleRole(_cid); } }}
              >
                {on && (
                  <button type="button" className="df-role-card__remove" aria-label="Remove custom role" onClick={e => { e.stopPropagation(); handleToggleRole(_cid); }}>
                    <svg viewBox="0 0 12 12" width="8" height="8" fill="none" stroke="currentColor" strokeWidth="2.2" strokeLinecap="round" aria-hidden="true"><line x1="2" y1="2" x2="10" y2="10"/><line x1="10" y1="2" x2="2" y2="10"/></svg>
                  </button>
                )}
                <span className={`df-role-card__check ${on ? 'is-on' : ''}`} aria-hidden="true">
                  {on
                    ? <window.Check size={11} />
                    : <svg viewBox="0 0 16 16" width="11" height="11" fill="none" stroke="currentColor" strokeWidth="2.4" strokeLinecap="round" aria-hidden="true"><line x1="8" y1="3.5" x2="8" y2="12.5"/><line x1="3.5" y1="8" x2="12.5" y2="8"/></svg>}
                </span>
                <div className="df-role-card__name">{on && _name ? _name : 'Custom role'}</div>
                <div className="df-role-card__price">{on ? (isFT ? (_cc.customRate ? `From £${_cc.customRate}/hour` : 'Define your own role') : (_cc.customRate ? `£${_cc.customRate}/day` : 'Define your own role')) : 'Define your own role'}</div>
                {/* 2026-06-12 (Loom 41 0:30): the custom role recommends the
                    Philippines like the standard cards. */}
                <div className="df-role-card__rec">
                  <span>Recommended: </span>
                  <img src="https://flagcdn.com/ph.svg" alt="Philippines" title="Philippines" className="df-role-card__rec-flag" width="20" height="14" loading="lazy" />
                </div>
                <span className="df-role-card__glass" aria-hidden="true"></span>
              </div>
            );
          })()}
        </div>
      </div>

      {/* 2026-06-17: "How soon do you need them to start?" start-window
          selector removed from the dedicated-resources flow per request. */}

      {/* 2026-05-26: inline-config lifted OUT of df-roles-wrap so it is a
          direct sibling of df-deposit-card + df-forecast. Lets the
          sibling-selector + :has() merge CSS visually combine the three. */}
      {selectedRoles.length > 0 && focusedRole && (
          <div className="df-inline-config">
            {selectedRoles.length > 1 && (
              <div className="df-tab-strip" role="tablist">
                {selectedRoles.map(rid => {
                  const r = dfRoleById(service, tier, rid);
                  if (!r) return null;
                  const _rNm = (rid === 'ft-custom' || rid === 'pt-custom') ? ((String((cfgs[rid] || {}).customName || '').trim()) || 'Custom role') : r.name;
                  const tabOn = rid === focusedRoleId;
                  return (
                    // 2026-06-08: each role tab carries an X so users can
                    // deselect that role straight from the tab (not just the
                    // role card above). The label stays a real tab button for
                    // keyboard/focus; the X is a sibling button that removes
                    // the role and the focus effect re-points to a remaining one.
                    <div
                      key={rid}
                      className={`df-tab ${tabOn ? 'df-tab--on' : ''}`}
                      role="presentation"
                    >
                      <button
                        type="button"
                        role="tab"
                        aria-selected={tabOn}
                        className="df-tab__name"
                        onClick={() => setFocusedRoleId(rid)}
                      >
                        {_rNm}
                      </button>
                      <button
                        type="button"
                        className="df-tab__close"
                        aria-label={`Remove ${_rNm}`}
                        title={`Remove ${_rNm}`}
                        onClick={(e) => { e.stopPropagation(); onToggleRole(rid); }}
                      >
                        <svg viewBox="0 0 16 16" width="11" height="11" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" aria-hidden="true">
                          <path d="M4 4l8 8M12 4l-8 8" />
                        </svg>
                      </button>
                    </div>
                  );
                })}
              </div>
            )}

            <div className="df-inline-config__body" key={focusedRoleId}>
              {focusedRole && focusedRole.isCustom && isFT ? (
                <DFCustomForm
                  cfg={focusedCfg}
                  onPatch={patch => onSetRoleConfig(focusedRoleId, patch)}
                  depositCard={selectedRoles.length > 0 ? (
                    <DFDepositCard isFT={true} lines={ftLines} ftMonthly={ftMonthly} ptMonthly={ptMonthly} selectedRoles={selectedRoles} totalHires={selectedRoles.reduce((s, rid) => s + (isFT ? Math.max(1, Math.min(99, cfgs[rid]?.qty || 1)) : 1), 0)} deposit={DEDICATED_DEPOSIT} showBuyout={false} onOpenInfo={setInfoRoleId} onFocusRole={setFocusedRoleId} />
                  ) : null}
                />
              ) : isFT ? (
                <DFFullTimeForm
                  role={focusedRole}
                  cfg={focusedCfg}
                  onPatch={patch => onSetRoleConfig(focusedRoleId, patch)}
                  depositCard={selectedRoles.length > 0 ? (
                    <DFDepositCard isFT={isFT} lines={ftLines} ftMonthly={ftMonthly} ptMonthly={ptMonthly} selectedRoles={selectedRoles} totalHires={selectedRoles.reduce((s, rid) => s + (isFT ? Math.max(1, Math.min(99, cfgs[rid]?.qty || 1)) : 1), 0)} deposit={DEDICATED_DEPOSIT} showBuyout={buyoutEligible} onOpenInfo={setInfoRoleId} onFocusRole={setFocusedRoleId} />
                  ) : null}
                />
              ) : (
                <DFDayRateTiles
                  role={focusedRole}
                  cfg={focusedCfg}
                  onPatch={patch => onSetRoleConfig(focusedRoleId, patch)}
                  tier={tier}
                  ptBilling={ptBilling}
                  depositCard={selectedRoles.length > 0 ? (
                    <DFDepositCard isFT={isFT} lines={ptLines} ftMonthly={ftMonthly} ptMonthly={ptMonthly} selectedRoles={selectedRoles} totalHires={selectedRoles.length} deposit={DEDICATED_DEPOSIT} showBuyout={false} onOpenInfo={setInfoRoleId} ptBilling={ptBilling} onSetPtBilling={onSetPtBilling} onFocusRole={setFocusedRoleId} />
                  ) : null}
                />
              )}
            </div>
          </div>
        )}

      {/* §3.7, Collapsible "in-house cost comparison" forecast panel. Only
          for FT (PT compares against UK freelance day rates, different lookup).
          Reads window.drFtForecast(state) which uses DR_FT_UK_BASE_SALARY × 1.4
          for the in-house side and hourlyFrom × hours × commit multiplier for
          the GoGorilla side. */}
      {DF_SHOW_INHOUSE_COMPARISON && isFT && selectedRoles.length > 0 && window.drFtForecast && (() => {
        const fc = window.drFtForecast(window.__lastBuildPageState);
        if (fc.gorilla.total === 0) return null;
        return (
          <details className="df-forecast">
            <summary className="df-forecast__trigger">
              <span className="df-forecast__trigger-label">See the in-house cost comparison</span>
              <svg className="df-forecast__chevron" viewBox="0 0 12 12" width="11" height="11" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
                <polyline points="3 4.5 6 8 9 4.5"/>
              </svg>
            </summary>
            <div className="df-forecast__panel">
              <div className="df-forecast__cols">
                <div className="df-forecast__col df-forecast__col--gorilla">
                  <div className="df-forecast__col-head">GoGorilla.com · 12 months</div>
                  <ul className="df-forecast__rows">
                    {fc.gorilla.rows.map(r => (
                      <li key={r.roleId}>
                        <span>{r.name}</span>
                        <span>{window.fmt(r.annual)}</span>
                      </li>
                    ))}
                  </ul>
                  <div className="df-forecast__total">{window.fmt(fc.gorilla.total)}</div>
                </div>
                <div className="df-forecast__col df-forecast__col--inhouse">
                  <div className="df-forecast__col-head">In-house UK · 12 months</div>
                  <ul className="df-forecast__rows">
                    {fc.inhouse.rows.map(r => (
                      <li key={r.roleId}>
                        <span>{r.name}</span>
                        <span>{window.fmt(r.annual)}</span>
                      </li>
                    ))}
                  </ul>
                  <div className="df-forecast__total">{window.fmt(fc.inhouse.total)}</div>
                </div>
              </div>
              {fc.savings > 0 && (
                <div className="df-forecast__savings">
                  Save <strong>{window.fmt(fc.savings)}</strong> over 12 months by going with GoGorilla.com.
                </div>
              )}
              <div className="df-forecast__disclaimer">
                Indicative comparison. GoGorilla.com pricing uses commit-length rates across Philippines, South Africa, and UK location mix; in-house side uses UK loaded cost (base salary × 1.4 for NI, pension, benefits, equipment, and recruitment). Final numbers confirmed at kickoff.
              </div>
            </div>
          </details>
        );
      })()}
      {infoRoleId && window.GMOverlayModal && (window.DEDICATED_ROLE_OVERLAYS || {})[infoRoleId] && (
        <window.GMOverlayModal
          data={(window.DEDICATED_ROLE_OVERLAYS || {})[infoRoleId]}
          onClose={() => setInfoRoleId(null)}
          serviceId={service && service.id}
        />
      )}
    </div>
  );
}

window.DedicatedFlow = DedicatedFlow;
