/**
 * Demo Mode configuration (§29, §51).
 *
 * Demo Mode runs the platform against a *pinned virtual clock* rather than the wall
 * clock. That is a deliberate choice: the failure story is a peak-summer event, and a
 * judge running the build in January should still see it. Everything else — the risk
 * scores, the ETA, the recommended actions, the post-action risk — is computed live by
 * the same engine that serves the real-time view. Nothing below is a display constant.
 *
 * The perturbation parameters were calibrated against the engine (scripts/calibrate.ts):
 * at stage 4 they place T-108 at ~90 % loading, ~91 °C top-oil, risk ≈ 78 and a first
 * limit breach ≈ 44 minutes out.
 */

import type { Perturbation } from '../engine/telemetry'

/** The asset the story follows. */
export const DEMO_ASSET_CODE = 'T-108'
export const DEMO_REGION_CODE = 'RYD'

/** Local Asia/Riyadh wall-clock instant the story is pinned to: 29 August, 12:45. */
export const DEMO_MONTH_INDEX = 7
export const DEMO_DAY_OF_MONTH = 29
export const DEMO_LOCAL_HOUR = 12.75

const MS_PER_HOUR = 3_600_000
const KSA_OFFSET_HOURS = 3

/**
 * The pinned instant, resolved against the current year so the displayed date stays
 * plausible while the season (and therefore the physics) stays fixed.
 */
export function demoBaseTimestamp(now = Date.now()): number {
  const year = new Date(now).getUTCFullYear()
  const localMidnightUtc = Date.UTC(year, DEMO_MONTH_INDEX, DEMO_DAY_OF_MONTH) - KSA_OFFSET_HOURS * MS_PER_HOUR
  return localMidnightUtc + DEMO_LOCAL_HOUR * MS_PER_HOUR
}

/**
 * Full-strength scenario: a peak-heat afternoon plus an unplanned industrial surge that
 * ramps in over roughly an hour and a half.
 *
 * The ramp is what makes the story physical rather than a step function — demand arrives
 * progressively, the oil temperature chases it with its own lag, and the predicted time
 * of failure is therefore a real crossing of a thermal limit rather than a hair's-breadth
 * threshold comparison.
 */
export const DEMO_RAMP_LEAD_MIN = 40
export const DEMO_RAMP_MINUTES = 66
export const DEMO_LOAD_DELTA_PCT = 40
export const DEMO_TEMP_DELTA_C = 1.75

/** Scenario parameters without the runtime-dependent ramp anchor. */
export const DEMO_PERTURBATION: Perturbation = {
  tempDeltaC: DEMO_TEMP_DELTA_C,
  loadDeltaPct: DEMO_LOAD_DELTA_PCT,
  rampMinutes: DEMO_RAMP_MINUTES,
  regionCode: DEMO_REGION_CODE,
}

/** The scenario anchored to the pinned clock, ready to hand to the engine. */
export function demoPerturbation(now = Date.now()): Perturbation {
  return {
    ...DEMO_PERTURBATION,
    rampStartMs: demoBaseTimestamp(now) - DEMO_RAMP_LEAD_MIN * 60_000,
  }
}

export interface DemoStage {
  index: number
  key: string
  title: string
  titleAr: string
  narration: string
  narrationAr: string
  /** Minutes before/after the pinned instant this stage is observed at. */
  offsetMin: number
  /** Whether the preventive strategy has been applied. */
  actionsApplied: boolean
  /** Which panel the presentation view should foreground. */
  focus:
    | 'grid'
    | 'asset'
    | 'weak_signal'
    | 'dna'
    | 'prediction'
    | 'root_cause'
    | 'cascade'
    | 'explain'
    | 'time_machine'
    | 'counterfactual'
    | 'strategies'
    | 'recommendations'
    | 'simulation'
    | 'result'
    | 'physics'
    | 'uncertainty'
    | 'breakpoint'
    | 'memory'
    | 'pareto'
    | 'evidence'
    | 'verification'
    | 'validation'
    // 4.1
    | 'sources'
    | 'hunt'
    | 'replay'
}

export const DEMO_STAGES: DemoStage[] = [
  {
    index: 1,
    key: 'normal',
    title: 'Grid normal',
    titleAr: 'الشبكة في وضعها الطبيعي',
    narration:
      'Riyadh is inside its normal afternoon envelope. Every monitored asset sits in the green band and no operator action is pending.',
    narrationAr:
      'منطقة الرياض ضمن نطاقها الطبيعي بعد الظهر. جميع الأصول المراقبة في النطاق الأخضر ولا يوجد إجراء تشغيلي معلّق.',
    offsetMin: -40,
    actionsApplied: false,
    focus: 'grid',
  },
  {
    index: 2,
    key: 'load_rising',
    title: 'Load rising',
    titleAr: 'ارتفاع الحمل',
    narration:
      'An unscheduled industrial block comes online while ambient temperature is still climbing. Demand in the region starts to run ahead of the day-ahead profile.',
    narrationAr:
      'دخول كتلة أحمال صناعية غير مجدولة بينما لا تزال درجة الحرارة في ارتفاع. يبدأ الطلب في المنطقة بتجاوز المنحنى المخطط لليوم السابق.',
    offsetMin: -25,
    actionsApplied: false,
    focus: 'grid',
  },
  {
    index: 3,
    key: 'temperature_rising',
    title: 'Temperature increasing',
    titleAr: 'ارتفاع درجة الحرارة',
    narration:
      'Top-oil temperature on T-108 begins to track the new loading. Because oil responds with a lag of tens of minutes, a conventional threshold alarm is still silent.',
    narrationAr:
      'تبدأ حرارة الزيت العلوي في المحول T-108 بمتابعة التحميل الجديد. ولأن الزيت يستجيب بتأخر عشرات الدقائق، فإن إنذار العتبة التقليدي لا يزال صامتًا.',
    offsetMin: -12,
    actionsApplied: false,
    focus: 'asset',
  },
  {
    index: 4,
    key: 'weak_signal',
    title: 'Weak signals converge',
    titleAr: 'تقارب الإشارات الضعيفة',
    narration:
      'Four channels are each a few per cent away from what the twin expects for this asset at this hour — loading, top-oil temperature, cooling effectiveness and voltage. None of them is worth an alarm on its own. Together they form a pattern, and no conventional threshold has been crossed yet.',
    narrationAr:
      'أربع قنوات تبتعد كل واحدة بنسب قليلة عمّا يتوقعه التوأم الرقمي لهذا الأصل في هذه الساعة — التحميل وحرارة الزيت العلوي وكفاءة التبريد والجهد. لا تستحق أي منها إنذارًا بمفردها، لكنها مجتمعة تُشكّل نمطًا، ولم تُتجاوز أي عتبة تقليدية بعد.',
    offsetMin: -6,
    actionsApplied: false,
    focus: 'weak_signal',
  },
  {
    index: 5,
    key: 'dna_match',
    title: 'Failure DNA match detected',
    titleAr: 'اكتشاف تطابق مع بصمة عطل',
    narration:
      'The live signature of T-108 is compared against every recorded failure signature. One historical pattern comes back as a strong match.',
    narrationAr:
      'تُقارن البصمة الحية للمحول T-108 بجميع بصمات الأعطال المسجلة، فيظهر تطابق قوي مع أحد الأنماط التاريخية.',
    offsetMin: 0,
    actionsApplied: false,
    focus: 'dna',
  },
  {
    index: 6,
    key: 'physics',
    title: 'Is it physically possible?',
    titleAr: 'هل هو ممكن فيزيائيًا؟',
    narration:
      'A pattern match is a statement about similarity, not about physics. Before the platform will carry it further, the trajectory is checked against the plant: the emergency rating, the thermal limit, the ceiling this unit actually has today after twenty-three years of service and a 42 °C afternoon. The engine confirms the path is feasible — which is what makes the next number worth reading.',
    narrationAr:
      'تطابق النمط عبارة عن تشابه لا عن فيزياء. وقبل أن تمضي المنصة به أبعد، يُقاس المسار على المعدّة نفسها: التصنيف الطارئ، والحد الحراري، والسقف الذي يملكه هذا المحول فعلًا اليوم بعد ثلاثة وعشرين عامًا من الخدمة وبعد ظهيرة عند ٤٢°. يؤكد المحرك أن المسار ممكن — وهذا ما يجعل الرقم التالي جديرًا بالقراءة.',
    offsetMin: -6,
    actionsApplied: false,
    focus: 'physics',
  },
  {
    index: 7,
    key: 'prediction',
    title: 'Failure predicted',
    titleAr: 'التنبؤ بالعطل',
    narration:
      'The digital twin is walked forward minute by minute under current conditions. The first operating limit is crossed well inside the hour.',
    narrationAr:
      'يُشغَّل التوأم الرقمي للأمام دقيقة بدقيقة وفق الظروف الحالية، فيتبيّن تجاوز أول حد تشغيلي خلال أقل من ساعة.',
    offsetMin: 0,
    actionsApplied: false,
    focus: 'prediction',
  },
  {
    index: 8,
    key: 'uncertainty',
    title: 'How confident are we?',
    titleAr: 'ما مدى ثقتنا؟',
    narration:
      'The estimate alone would imply a precision the forward walk does not have. So the platform reports the shape of its belief instead: the probability, the window the crossing is likely to fall in, and four confidences kept apart — data, model, physics, twin — because they degrade for different reasons and call for different responses.',
    narrationAr:
      'التقدير وحده يوحي بدقة لا يملكها المسار الأمامي. لذا تعرض المنصة شكل اعتقادها بدلًا من ذلك: الاحتمال، والنافذة التي يرجّح أن يقع فيها التجاوز، وأربع ثقات منفصلة — البيانات والنموذج والفيزياء والتوأم — لأنها تنخفض لأسباب مختلفة وتستدعي استجابات مختلفة.',
    offsetMin: 0,
    actionsApplied: false,
    focus: 'uncertainty',
  },
  {
    index: 9,
    key: 'root_cause',
    title: 'Root cause',
    titleAr: 'السبب الجذري',
    narration:
      'The platform does not stop at "temperature is high". It scores the conditions that could produce this state and lays the winner out as a chain: ambient heat above the seasonal normal, compounded by demand ahead of profile, degrading cooling effectiveness and raising thermal stress on the unit.',
    narrationAr:
      'لا تكتفي المنصة بالقول إن «الحرارة مرتفعة». بل تُقيّم الحالات التي قد تُنتج هذا الوضع وتعرض أقواها كسلسلة: حرارة محيطة أعلى من المعدل الموسمي، يعززها طلب يتجاوز المنحنى المخطط، فتتراجع كفاءة التبريد ويرتفع الإجهاد الحراري على الوحدة.',
    offsetMin: 0,
    actionsApplied: false,
    focus: 'root_cause',
  },
  {
    index: 10,
    key: 'cascade',
    title: 'Where it would spread',
    titleAr: 'إلى أين ينتشر',
    narration:
      'If T-108 gives way, its load does not disappear — it moves. The cascade engine walks that hand-off across the grid graph and reports where it stops. Here it stops at the origin: the sibling units on the same busbar hold enough headroom to absorb the transfer. That headroom is not an abstraction — it is the same capacity the recommended strategy is about to spend.',
    narrationAr:
      'إذا استسلم المحول T-108 فإن حمله لا يختفي بل ينتقل. يمشي محرك الأعطال المتتالية على هذا الانتقال عبر الرسم الشبكي ويحدد أين يتوقف. وهنا يتوقف عند المصدر: الوحدات الشقيقة على القضيب نفسه تملك هامشًا كافيًا لاستيعاب التحويل. وهذا الهامش ليس مفهومًا مجردًا — بل هو السعة ذاتها التي ستنفقها الاستراتيجية الموصى بها.',
    offsetMin: 0,
    actionsApplied: false,
    focus: 'cascade',
  },
  {
    index: 11,
    key: 'breakpoint',
    title: 'Where the chain can be cut',
    titleAr: 'أين يمكن قطع السلسلة',
    narration:
      'Knowing where a cascade spreads is only half the answer. The break-point search ranks every node in the chain by the impact a cut there removes, divided by what the cut costs — and the best place is rarely the first or the last. For a cascade contained at the origin, the honest answer is that there is no chain to cut, and the platform says so rather than inventing one.',
    narrationAr:
      'معرفة أين ينتشر التسلسل نصف الإجابة فقط. يرتّب بحث نقاط القطع كل عقدة في السلسلة وفق الأثر الذي يزيله القطع عندها مقسومًا على تكلفته — وأفضل موضع نادرًا ما يكون الأول أو الأخير. وحين يكون التسلسل محتوى عند المصدر فالإجابة الصادقة أنه لا سلسلة لقطعها، وتقولها المنصة بدل اختلاق واحدة.',
    offsetMin: 0,
    actionsApplied: false,
    focus: 'breakpoint',
  },
  {
    index: 12,
    key: 'explain',
    title: 'Explainable AI — WHY?',
    titleAr: 'الذكاء القابل للتفسير — لماذا؟',
    narration:
      'Every point of the risk score is attributed back to a measurable factor, so the operator can audit the reasoning instead of trusting it.',
    narrationAr:
      'تُنسب كل نقطة من درجة الخطر إلى عامل قابل للقياس، بحيث يستطيع المشغل مراجعة المنطق بدلًا من الوثوق به فقط.',
    offsetMin: 0,
    actionsApplied: false,
    focus: 'explain',
  },
  {
    index: 13,
    key: 'time_machine',
    title: 'Energy Time Machine',
    titleAr: 'آلة الزمن للطاقة',
    narration:
      'The unmitigated trajectory is played forward so the consequence of doing nothing is visible before it happens.',
    narrationAr:
      'يُعرض المسار دون تدخل إلى الأمام لتظهر نتيجة عدم اتخاذ أي إجراء قبل وقوعها.',
    offsetMin: 0,
    actionsApplied: false,
    focus: 'time_machine',
  },
  {
    index: 14,
    key: 'memory',
    title: 'Have we seen this before?',
    titleAr: 'هل واجهنا هذا من قبل؟',
    narration:
      'The situation is compared against every decision the platform has recorded — not by asset, but by shape: loading, heat, health, hour, season, pattern, root cause. Each match arrives with the per-field contributions that produced its score, so the similarity can be checked rather than taken on trust. Every one of these outcomes is simulated, and is labelled as such.',
    narrationAr:
      'يُقارن الموقف بكل قرار سجّلته المنصة — لا حسب الأصل بل حسب الشكل: التحميل والحرارة والصحة والساعة والموسم والنمط والسبب الجذري. ويأتي كل تطابق مصحوبًا بمساهمات الحقول التي أنتجت درجته، فيمكن فحص التشابه لا مجرد تصديقه. وكل هذه النتائج محاكاة، وموسومة بذلك.',
    offsetMin: 0,
    actionsApplied: false,
    focus: 'memory',
  },
  {
    index: 15,
    key: 'counterfactual',
    title: 'The smallest change that prevents it',
    titleAr: 'أقل تغيير يمنع الحدث',
    narration:
      'The question is inverted. Not "what will happen" but "what is the least we can do". The search bisects each available lever down to the smallest dose that still clears the target, so the recommendation spends the minimum of a finite resource.',
    narrationAr:
      'يُقلب السؤال. ليس «ماذا سيحدث» بل «ما أقل ما يمكن فعله». يقلّص البحث كل إجراء متاح إلى أصغر جرعة لا تزال تحقق الهدف، فتنفق التوصية أقل قدر من مورد محدود.',
    offsetMin: 0,
    actionsApplied: false,
    focus: 'counterfactual',
  },
  {
    index: 16,
    key: 'pareto',
    title: 'The options around the answer',
    titleAr: 'الخيارات حول الإجابة',
    narration:
      'The minimum intervention is one answer to one objective. The frontier shows the others: the cheapest, the fastest, the gentlest on customers, and the one that balances them. Each is a genuine trade rather than a worse version of the recommendation — and for every option that is not recommended, the platform states which axis it loses on and by how much.',
    narrationAr:
      'أقل تدخل هو إجابة واحدة لهدف واحد. تعرض الجبهة البقية: الأرخص والأسرع والأخف على المشتركين والذي يوازن بينها. وكل منها مفاضلة حقيقية لا نسخة أسوأ من التوصية — ولكل خيار غير موصى به تذكر المنصة المحور الذي يخسر فيه ومقداره.',
    offsetMin: 0,
    actionsApplied: false,
    focus: 'pareto',
  },
  {
    index: 17,
    key: 'strategies',
    title: 'Strategies compared',
    titleAr: 'مقارنة الاستراتيجيات',
    narration:
      'Several genuinely different recovery strategies are constructed and each one is run through the digital twin. They are scored on one objective — risk reduction, cost, customer impact, execution time and five more terms — and one of them wins on the numbers rather than on preference.',
    narrationAr:
      'تُبنى عدة استراتيجيات تعافٍ مختلفة فعليًا، وتُشغَّل كل واحدة على التوأم الرقمي. ثم تُقيَّم بهدف واحد — خفض الخطر والتكلفة وأثر المشتركين وزمن التنفيذ وخمسة بنود أخرى — فتفوز إحداها بالأرقام لا بالتفضيل.',
    offsetMin: 0,
    actionsApplied: false,
    focus: 'strategies',
  },
  {
    index: 18,
    key: 'recommendations',
    title: 'AI recommends preventive actions',
    titleAr: 'توصيات وقائية من الذكاء الاصطناعي',
    narration:
      'Candidate actions are sized against the resources actually available around the asset, and each is scored by re-running the twin with that action applied.',
    narrationAr:
      'تُحدَّد الإجراءات المقترحة بحسب الموارد المتاحة فعليًا حول الأصل، ويُقيَّم كل إجراء بإعادة تشغيل التوأم الرقمي بعد تطبيقه.',
    offsetMin: 0,
    actionsApplied: false,
    focus: 'recommendations',
  },
  {
    index: 19,
    key: 'validation',
    title: 'Does the plan break a limit?',
    titleAr: 'هل تخالف الخطة حدًا؟',
    narration:
      'Before the recommendation reaches a human, it is measured against the engineering limits: the receiving feeders’ spare capacity, the battery’s available power, the contracted flexible load, the reserve margin, the thermal ceiling. A strategy that breaches a hard limit is returned rejected with the constraint that killed it — the validator refuses, it does not quietly shrink the dose until it fits.',
    narrationAr:
      'قبل أن تصل التوصية إلى إنسان، تُقاس على الحدود الهندسية: السعة الاحتياطية للمغذيات المستقبِلة، والقدرة المتاحة للبطارية، والحمل المرن المتعاقد عليه، وهامش الاحتياطي، والسقف الحراري. والاستراتيجية التي تخالف حدًا صارمًا تُعاد مرفوضة مع القيد الذي أوقفها — يرفض المدقق ولا يقلّص الجرعة بصمت حتى تناسب.',
    offsetMin: 0,
    actionsApplied: false,
    focus: 'validation',
  },
  {
    index: 20,
    key: 'simulate',
    title: 'Simulate the combined strategy',
    titleAr: 'محاكاة الاستراتيجية المدمجة',
    narration:
      'The combined strategy is simulated against the same forward model, at the same moment the failure was predicted for.',
    narrationAr:
      'تُحاكى الاستراتيجية المدمجة على النموذج المستقبلي نفسه، وفي اللحظة ذاتها التي تنبأ النظام بوقوع العطل فيها.',
    offsetMin: 0,
    actionsApplied: true,
    focus: 'simulation',
  },
  {
    index: 21,
    key: 'evidence',
    title: 'PROVE IT',
    titleAr: 'أثبت ذلك',
    narration:
      'The whole chain, assembled from what actually ran: the readings and their provenance, the quality score that bounded the confidence, the pattern that matched, the physics check that passed, the twin it was walked on, the cascade, the simulation, the recommendation and its projected impact. Every node carries its source, the weakest link is named, and any stage that did not run is listed as missing rather than quietly omitted.',
    narrationAr:
      'السلسلة كاملة، مجمّعة مما جرى فعلًا: القراءات ومصادرها، ودرجة الجودة التي حدّت الثقة، والنمط الذي طابق، والفحص الفيزيائي الذي نجح، والتوأم الذي مُشي عليه، والتسلسل، والمحاكاة، والتوصية وأثرها المُسقط. كل عقدة تحمل مصدرها، وأضعف حلقة مسمّاة، وأي مرحلة لم تُنفَّذ تُدرج كمفقودة لا تُحذف بصمت.',
    offsetMin: 0,
    actionsApplied: true,
    focus: 'evidence',
  },
  {
    index: 22,
    key: 'prevented',
    title: 'Potential failure prevented',
    titleAr: 'تم تفادي عطل محتمل',
    narration:
      'With the strategy in force the asset stays inside its operating envelope through the peak. The avoided-impact figures below are demo estimates derived from the simulation.',
    narrationAr:
      'مع تطبيق الاستراتيجية يبقى الأصل داخل نطاقه التشغيلي طوال الذروة. الأرقام أدناه تقديرات عرضية مشتقة من المحاكاة.',
    offsetMin: 0,
    actionsApplied: true,
    focus: 'result',
  },
  {
    index: 23,
    key: 'verification',
    title: 'And was it right?',
    titleAr: 'وهل كان صحيحًا؟',
    narration:
      'The last act is the one that makes the rest falsifiable. Every prediction the platform has made is checked against what the model actually did at its horizon, channel by channel — and the record includes the predictions still waiting, because a coverage figure computed only over the checked ones is a survivorship figure. The twin’s fidelity moves by the result, slowly upward for a good comparison and faster downward for a bad one.',
    narrationAr:
      'الفصل الأخير هو ما يجعل ما قبله قابلًا للتكذيب. كل توقع أصدرته المنصة يُقاس على ما فعله النموذج فعلًا عند أفقه، قناةً بقناة — ويشمل السجل التوقعات التي ما زالت تنتظر، لأن نسبة تغطية محسوبة على المتحقق منها فقط نسبة ناجين. وتتحرك دقة التوأم بالنتيجة: صعودًا ببطء عند مقارنة جيدة، وهبوطًا أسرع عند مقارنة سيئة.',
    offsetMin: 0,
    actionsApplied: true,
    focus: 'verification',
  },
  {
    index: 24,
    key: 'sources',
    title: 'Where this came from',
    titleAr: 'من أين جاء هذا',
    narration:
      'Everything above rests on what the platform was fed. Six demonstration sources are delivering; each is labelled DEMO CONNECTED because none of them reaches an external system, and the mean input quality is capped by the weakest one rather than averaged with it.',
    narrationAr:
      'كل ما سبق يقوم على ما غُذيت به المنصة. ستة مصادر تجريبية ترسل الآن؛ كل منها موسوم «متصل تجريبيًا» لأن أيًا منها لا يصل إلى نظام خارجي، ومتوسط جودة المدخلات مقيد بأضعف مصدر لا ممزوج به.',
    offsetMin: 30,
    actionsApplied: true,
    focus: 'sources',
  },
  {
    index: 25,
    key: 'hunt',
    title: 'What nobody asked about',
    titleAr: 'ما لم يسأل عنه أحد',
    narration:
      'Every search so far answered a question somebody asked. This one does not: the hunter searches for failures nobody has imagined, and reports the budget it spent so a short search is never mistaken for an exhaustive one.',
    narrationAr:
      'كل بحث حتى الآن أجاب سؤالًا طرحه أحد. هذا البحث لا يفعل: يبحث الصائد عن أعطال لم يتخيلها أحد، ويصرّح بالميزانية التي أنفقها حتى لا يُقرأ بحث قصير على أنه مسح شامل.',
    offsetMin: 30,
    actionsApplied: true,
    focus: 'hunt',
  },
  {
    index: 26,
    key: 'replay',
    title: 'Prove it',
    titleAr: 'أثبت ذلك',
    narration:
      'The whole incident, from the first weak signal to the outcome, read back from the events the platform recorded rather than recomputed. The prevention window is the time an operator actually had — and the outcome is labelled synthetic, because nothing here prevented a real failure.',
    narrationAr:
      'الحادثة كاملة، من أول إشارة ضعيفة إلى النتيجة، مقروءة من الأحداث التي سجلتها المنصة لا محسوبة من جديد. نافذة المنع هي الوقت الذي أتيح فعلًا للمشغل — والنتيجة موسومة بأنها اصطناعية، لأن لا شيء هنا منع عطلًا حقيقيًا.',
    offsetMin: 30,
    actionsApplied: true,
    focus: 'replay',
  },
]

export const DEMO_STAGE_COUNT = DEMO_STAGES.length

export function getDemoStage(index: number): DemoStage {
  return DEMO_STAGES[Math.max(0, Math.min(DEMO_STAGES.length - 1, index - 1))]
}

/**
 * The perturbation in force at a given stage.
 *
 * There is no per-stage scaling factor: the surge ramps on the clock, and each stage
 * simply observes it at a different moment. The build-up an audience sees across
 * stages 1–4 is the model's own trajectory, not a presentation dial.
 */
export function demoPerturbationFor(_stage: DemoStage, now = Date.now()): Perturbation {
  return demoPerturbation(now)
}

/** The virtual instant a given stage is observed at. */
export function demoTimestampFor(stage: DemoStage, now = Date.now()): number {
  return demoBaseTimestamp(now) + stage.offsetMin * 60_000
}
