{"version":3,"sources":["../src/index.ts","../src/parser.ts","../src/file-input.ts","../src/parse-file.ts","../src/geo.ts","../src/analyzer.ts"],"sourcesContent":["export { parse } from './parser.js'\nexport type { ParseOptions } from './parser.js'\nexport { parseFile } from './parse-file.js'\nexport type { ParseFileOptions } from './parse-file.js'\nexport { analyze, formatPace, formatDistance, formatDuration } from './analyzer.js'\nexport type { AnalyzeOptions, HrZoneModel } from './analyzer.js'\nexport type {\n  Activity,\n  TrackPoint,\n  ActivityStats,\n  Split,\n  HeartRateZones,\n  ChartOptions,\n  ChartType,\n} from './types.js'\n","import { XMLParser } from 'fast-xml-parser'\nimport { Decoder, Stream } from '@garmin/fitsdk'\nimport { readInputFile } from './file-input.js'\nimport type { Activity, TrackPoint } from './types.js'\n\nconst xmlParser = new XMLParser({\n  ignoreAttributes: false,\n  attributeNamePrefix: '@_',\n  parseAttributeValue: true,\n})\n\nfunction asArray<T>(v: T | T[] | undefined): T[] {\n  if (v == null) return []\n  return Array.isArray(v) ? v : [v]\n}\n\n// Coerce a parsed-XML value to a finite number, or undefined. Malformed\n// markup (a non-numeric string, an unexpected element shape that coerces to\n// NaN) must be dropped at the boundary: NaN passes every `!= null` guard\n// downstream and silently corrupts avgHeartRate / maxHeartRate / hrZones.\nfunction finiteNumber(v: unknown): number | undefined {\n  // Empty elements (<HeartRateBpm/>) parse to '' and would coerce to 0 —\n  // a fabricated reading, not a recorded one — so they are dropped too.\n  if (v == null || (typeof v === 'string' && v.trim() === '')) return undefined\n  const n = Number(v)\n  return Number.isFinite(n) ? n : undefined\n}\n\n// Same boundary rule for timestamps: an unparseable time string yields an\n// Invalid Date whose getTime() is NaN, which would poison every duration\n// computed from it.\nfunction validDate(v: unknown): Date | undefined {\n  const d = new Date(v as string | number | Date)\n  return isNaN(d.getTime()) ? undefined : d\n}\n\nexport function parseGpx(xml: string): Activity {\n  const doc = xmlParser.parse(xml)\n  const gpx = doc.gpx\n\n  const tracks = asArray(gpx?.trk)\n  const firstTrack = tracks[0]\n\n  const name: string | undefined =\n    gpx?.metadata?.name ?? firstTrack?.name ?? undefined\n\n  const type: string | undefined =\n    firstTrack?.type?.toString().toLowerCase() ?? undefined\n\n  const points: TrackPoint[] = []\n\n  for (const trk of tracks) {\n    for (const seg of asArray(trk?.trkseg)) {\n      for (const pt of asArray(seg?.trkpt)) {\n        const lat = parseFloat(pt['@_lat'])\n        const lon = parseFloat(pt['@_lon'])\n        if (isNaN(lat) || isNaN(lon)) continue\n\n        const point: TrackPoint = { lat, lon }\n\n        const ele = finiteNumber(pt.ele)\n        if (pt.ele != null && ele != null) point.elevation = ele\n        if (pt.time) point.timestamp = validDate(pt.time)\n\n        // Garmin extensions (heart rate + cadence)\n        const ext = pt.extensions\n        if (ext) {\n          const tpx =\n            ext['gpxtpx:TrackPointExtension'] ??\n            ext['ns3:TrackPointExtension'] ??\n            ext.TrackPointExtension\n\n          if (tpx) {\n            const hr =\n              tpx['gpxtpx:hr'] ?? tpx['ns3:hr'] ?? tpx.hr\n            const cad =\n              tpx['gpxtpx:cad'] ?? tpx['ns3:cad'] ?? tpx.cad\n\n            const hrNum = finiteNumber(hr)\n            if (hrNum != null) point.heartRate = Math.round(hrNum)\n            const cadNum = finiteNumber(cad)\n            if (cadNum != null) point.cadence = Math.round(cadNum) * 2 // Garmin stores per-foot cadence\n          }\n        }\n\n        points.push(point)\n      }\n    }\n  }\n\n  return {\n    name,\n    type,\n    startTime: points[0]?.timestamp,\n    points,\n    format: 'gpx',\n  }\n}\n\n// ---------------------------------------------------------------------------\n// TCX (Garmin Training Center XML) — Garmin / Strava / Wahoo exports\n// ---------------------------------------------------------------------------\n\nexport function parseTcx(xml: string): Activity {\n  const doc = xmlParser.parse(xml)\n  const activities = asArray(doc?.TrainingCenterDatabase?.Activities?.Activity)\n  const first = activities[0]\n\n  const type: string | undefined =\n    first?.['@_Sport']?.toString().toLowerCase() ?? undefined\n\n  const points: TrackPoint[] = []\n  // Lap-level <DistanceMeters> is the device's own total for that lap —\n  // distinct from the per-trackpoint <DistanceMeters> read below. A\n  // multi-lap file carries one per lap; the activity's device total is\n  // their sum.\n  let deviceDistanceM: number | undefined\n\n  for (const activity of activities) {\n    for (const lap of asArray(activity?.Lap)) {\n      const lapDist = Number(lap?.DistanceMeters)\n      if (!isNaN(lapDist)) deviceDistanceM = (deviceDistanceM ?? 0) + lapDist\n\n      for (const track of asArray(lap?.Track)) {\n        for (const tp of asArray(track?.Trackpoint)) {\n          const pos = tp?.Position\n          if (!pos) continue // GPS-less sample (indoor / paused)\n\n          const lat = Number(pos.LatitudeDegrees)\n          const lon = Number(pos.LongitudeDegrees)\n          if (isNaN(lat) || isNaN(lon)) continue\n\n          const point: TrackPoint = { lat, lon }\n\n          const tcxEle = finiteNumber(tp.AltitudeMeters)\n          if (tp.AltitudeMeters != null && tcxEle != null) point.elevation = tcxEle\n          if (tp.Time) point.timestamp = validDate(tp.Time)\n\n          // <DistanceMeters> is cumulative distance from the activity start.\n          if (tp.DistanceMeters != null) {\n            const d = Number(tp.DistanceMeters)\n            if (!isNaN(d)) point.distanceM = d\n          }\n\n          // <HeartRateBpm><Value>142</Value></HeartRateBpm>\n          const hr = finiteNumber(tp.HeartRateBpm?.Value ?? tp.HeartRateBpm)\n          if (hr != null) point.heartRate = hr\n\n          // Garmin activity extension (namespaced ns3: or bare):\n          // <Extensions><TPX><RunCadence>85</RunCadence></TPX></Extensions>\n          const ext = tp.Extensions\n          if (ext) {\n            const tpx = ext['ns3:TPX'] ?? ext.TPX\n            const cad = finiteNumber(tpx?.['ns3:RunCadence'] ?? tpx?.RunCadence)\n            // TCX RunCadence is per-foot RPM; double to steps/min (matches GPX/FIT).\n            if (cad != null) point.cadence = Math.round(cad * 2)\n          }\n\n          points.push(point)\n        }\n      }\n    }\n  }\n\n  const startTime = (first?.Id ? validDate(first.Id) : undefined) ?? points[0]?.timestamp\n\n  return { type, startTime, points, format: 'tcx', deviceDistanceM }\n}\n\n// ---------------------------------------------------------------------------\n// FIT (binary) — Garmin / Wahoo / Coros / Suunto device files\n// ---------------------------------------------------------------------------\n\nconst SEMICIRCLE_TO_DEG = 180 / 2 ** 31\n\nexport function toUint8(bytes: Uint8Array | ArrayBuffer | ArrayLike<number>): Uint8Array {\n  if (bytes instanceof Uint8Array) return bytes\n  if (bytes instanceof ArrayBuffer) return new Uint8Array(bytes)\n  return Uint8Array.from(bytes as ArrayLike<number>)\n}\n\n/** Quick check for the \".FIT\" signature in a binary buffer. */\nexport function isFit(bytes: Uint8Array): boolean {\n  try {\n    return Decoder.isFIT(Stream.fromByteArray(bytes))\n  } catch {\n    return false\n  }\n}\n\nexport function parseFit(bytes: Uint8Array): Activity {\n  const stream = Stream.fromByteArray(bytes)\n  if (!Decoder.isFIT(stream)) {\n    throw new Error('Unsupported file format. Supported formats: GPX, TCX, FIT.')\n  }\n\n  const decoder = new Decoder(stream)\n  // Defaults already convert timestamps to Date and enum types to strings.\n  const { messages } = decoder.read()\n\n  const records: Record<string, unknown>[] = messages.recordMesgs ?? []\n  const points: TrackPoint[] = []\n\n  for (const r of records) {\n    const rawLat = r.positionLat as number | undefined\n    const rawLon = r.positionLong as number | undefined\n    if (rawLat == null || rawLon == null) continue // GPS-less sample (indoor / paused)\n\n    const lat = rawLat * SEMICIRCLE_TO_DEG\n    const lon = rawLon * SEMICIRCLE_TO_DEG\n    if (isNaN(lat) || isNaN(lon)) continue\n\n    const point: TrackPoint = { lat, lon }\n\n    // enhancedAltitude is higher-resolution when present\n    const ele = (r.enhancedAltitude ?? r.altitude) as number | undefined\n    if (ele != null) point.elevation = ele\n\n    // enhancedDistance is higher-resolution when present; both are cumulative\n    // distance from the activity start, in metres (same read as altitude).\n    const dist = (r.enhancedDistance ?? r.distance) as number | undefined\n    if (dist != null) point.distanceM = dist\n\n    if (r.timestamp != null) point.timestamp = new Date(r.timestamp as string | number | Date)\n\n    const hr = r.heartRate as number | undefined\n    if (hr != null) point.heartRate = hr\n\n    // FIT stores running cadence as RPM (one foot); double to steps/min,\n    // matching the GPX path. fractionalCadence adds sub-step precision.\n    const cad = r.cadence as number | undefined\n    if (cad != null) {\n      const frac = (r.fractionalCadence as number | undefined) ?? 0\n      point.cadence = Math.round((cad + frac) * 2)\n    }\n\n    points.push(point)\n  }\n\n  const sport = (messages.sportMesgs?.[0] ?? {}) as Record<string, unknown>\n  const sessions = (messages.sessionMesgs ?? []) as Record<string, unknown>[]\n  const session = (sessions[0] ?? {}) as Record<string, unknown>\n\n  const name = (sport.name as string | undefined) ?? undefined\n  const type = ((sport.sport ?? session.sport) as string | undefined)?.toString().toLowerCase()\n  const startTime = session.startTime\n    ? new Date(session.startTime as string | number | Date)\n    : points[0]?.timestamp\n\n  // session.totalDistance is the device's own total for that session\n  // (camelCase in the SDK's decoded output, not total_distance). A\n  // multisport file carries one session per sport; sum them for the\n  // activity's device total.\n  const sessionDistances = sessions\n    .map(s => s.totalDistance as number | undefined)\n    .filter((d): d is number => d != null && !isNaN(d))\n  const deviceDistanceM = sessionDistances.length > 0\n    ? sessionDistances.reduce((a, b) => a + b, 0)\n    : undefined\n\n  // session.totalAscent / totalDescent are the device's own barometric/fused\n  // elevation totals for that session (camelCase in the SDK's output, uint16\n  // metres). Sum across sessions the same way as totalDistance for multisport\n  // files. Left undefined when no session reports the field — many watches\n  // never write it, in which case the analyzer falls back to the GPS-altitude\n  // hysteresis filter.\n  const sumSessionField = (field: 'totalAscent' | 'totalDescent'): number | undefined => {\n    const values = sessions\n      .map(s => s[field] as number | undefined)\n      .filter((v): v is number => v != null && !isNaN(v))\n    return values.length > 0 ? values.reduce((a, b) => a + b, 0) : undefined\n  }\n  const deviceElevationGainM = sumSessionField('totalAscent')\n  const deviceElevationLossM = sumSessionField('totalDescent')\n\n  return {\n    name, type, startTime, points, format: 'fit',\n    deviceDistanceM, deviceElevationGainM, deviceElevationLossM,\n  }\n}\n\n// ---------------------------------------------------------------------------\n// Public entry point\n// ---------------------------------------------------------------------------\n\n/** Dispatch a decoded XML/text document to the right parser. */\nexport function parseText(text: string): Activity {\n  if (text.includes('<TrainingCenterDatabase')) return parseTcx(text)\n  if (text.includes('<gpx')) return parseGpx(text)\n  throw new Error('Unsupported file format. Supported formats: GPX, TCX, FIT.')\n}\n\nexport interface ParseOptions {\n  /** Skip format sniffing and parse `input` as this format directly. */\n  format?: 'gpx' | 'tcx' | 'fit'\n}\n\nfunction truncate(s: string, max = 40): string {\n  return s.length > max ? `${s.slice(0, max)}…` : s\n}\n\nfunction isEnoent(err: unknown): err is NodeJS.ErrnoException {\n  return typeof err === 'object' && err !== null && (err as NodeJS.ErrnoException).code === 'ENOENT'\n}\n\n/**\n * Parse an activity file into a normalised {@link Activity}.\n *\n * Accepts:\n *  - a GPX or TCX file path (Node), or raw GPX/TCX XML string\n *  - a FIT file path (Node), or FIT bytes as `Uint8Array` / `ArrayBuffer`\n *\n * The format is auto-detected, so the same `analyze()` and chart configs work\n * for GPX, TCX and FIT input. Pass `{ format }` to skip auto-detection when\n * the format is already known.\n */\nexport function parse(input: string | Uint8Array | ArrayBuffer, options?: ParseOptions): Activity {\n  // Binary input is always FIT (GPX/TCX are text/XML).\n  if (typeof input !== 'string') {\n    return parseFit(toUint8(input))\n  }\n\n  // Explicit format: skip path/content sniffing entirely.\n  if (options?.format === 'gpx') return parseGpx(input)\n  if (options?.format === 'tcx') return parseTcx(input)\n  if (options?.format === 'fit') return parseFit(toUint8(Buffer.from(input, 'binary')))\n\n  // String that looks like a file path → read it and sniff the contents.\n  const isPath = !input.trimStart().startsWith('<') && input.length < 1000\n  if (isPath) {\n    let buf: Buffer\n    try {\n      buf = readInputFile(input)\n    } catch (err) {\n      if (isEnoent(err)) {\n        throw new Error(\n          `Stride: \"${truncate(input)}\" is neither a readable path nor recognisable GPX/TCX/FIT input.`,\n        )\n      }\n      throw err\n    }\n    const bytes = toUint8(buf)\n    if (isFit(bytes)) return parseFit(bytes)\n    return parseText(buf.toString('utf-8'))\n  }\n\n  // Raw string content.\n  return parseText(input)\n}\n","import fs from 'fs'\n\nexport function readInputFile(path: string): Buffer {\n  return fs.readFileSync(path)\n}\n","import { readFile } from 'fs/promises'\nimport { parseFit, parseGpx, parseTcx, parseText, isFit, toUint8 } from './parser.js'\nimport type { Activity } from './types.js'\n\nexport interface ParseFileOptions {\n  /** Skip format sniffing and parse the file as this format directly. */\n  format?: 'gpx' | 'tcx' | 'fit'\n}\n\n/**\n * Read and parse a GPX, TCX or FIT file from disk (Node only).\n *\n * Unlike {@link parse}, this always reads via `fs/promises` — no path\n * vs. raw-content sniffing is needed since `path` is unambiguously a file path.\n */\nexport async function parseFile(path: string, options?: ParseFileOptions): Promise<Activity> {\n  const buf = await readFile(path)\n\n  if (options?.format === 'gpx') return parseGpx(buf.toString('utf-8'))\n  if (options?.format === 'tcx') return parseTcx(buf.toString('utf-8'))\n  if (options?.format === 'fit') return parseFit(toUint8(buf))\n\n  const bytes = toUint8(buf)\n  if (isFit(bytes)) return parseFit(bytes)\n  return parseText(buf.toString('utf-8'))\n}\n","// ---------------------------------------------------------------------------\n// Shared geo helpers — distance maths lives here so analyzer.ts and\n// charts.ts never disagree about how far apart two points are.\n// ---------------------------------------------------------------------------\n\nconst R = 6_371_000 // Earth radius in metres (docs/metrics-spec.md Appendix A)\n\nexport function haversine(a: { lat: number; lon: number }, b: { lat: number; lon: number }): number {\n  const toRad = (d: number) => (d * Math.PI) / 180\n  const dLat = toRad(b.lat - a.lat)\n  const dLon = toRad(b.lon - a.lon)\n  const sinDLat = Math.sin(dLat / 2)\n  const sinDLon = Math.sin(dLon / 2)\n  const x =\n    sinDLat * sinDLat +\n    Math.cos(toRad(a.lat)) * Math.cos(toRad(b.lat)) * sinDLon * sinDLon\n  return 2 * R * Math.atan2(Math.sqrt(x), Math.sqrt(1 - x))\n}\n\n/**\n * Cumulative distance in metres, one entry per point, index 0 = 0.\n *\n * Always compute this over the full point set. Callers that need a series\n * for a downsampled/decimated set of points must sample *into* the result,\n * not sum haversine distances over the decimated points themselves — that\n * measures the chords of the decimated track, not the track, and undercounts\n * the true path length.\n */\nexport function cumulativeDistances(points: { lat: number; lon: number }[]): number[] {\n  const cumDist = new Array(points.length).fill(0)\n  for (let i = 1; i < points.length; i++) {\n    cumDist[i] = cumDist[i - 1] + haversine(points[i - 1], points[i])\n  }\n  return cumDist\n}\n\n/**\n * True when the device's own cumulative distance stream is usable for the\n * whole track: every point carries a finite `distanceM`, the series is\n * non-decreasing (a pause repeats a value — that is fine), and it is not all\n * zeros. A field that appears on only some points, goes backwards, or is\n * uniformly zero is treated as absent — half a device series is worse than\n * none, because splicing it onto haversine would put a discontinuity in the\n * cumulative distance.\n */\nexport function hasUsableDeviceDistance(points: { distanceM?: number }[]): boolean {\n  if (points.length === 0) return false\n  let prev = -Infinity\n  let max = 0\n  for (const p of points) {\n    const d = p.distanceM\n    if (d == null || !Number.isFinite(d)) return false\n    if (d < prev) return false            // non-monotonic → treat as absent\n    prev = d\n    if (d > max) max = d\n  }\n  return max > 0                          // all-zero → treat as absent\n}\n\n/**\n * Cumulative distance in metres, one entry per point, index 0 = 0, built from\n * whichever source is trustworthy for the *whole* track: the device's own\n * `distanceM` stream when {@link hasUsableDeviceDistance} holds, otherwise\n * summed haversine ({@link cumulativeDistances}). Never mix the two per\n * segment. The device series is re-based to the first point so it shares\n * haversine's frame (distance from the first recorded point, not from the\n * device's own zero, which may sit before the first GPS fix).\n *\n * This is the single series that must feed `distanceM`, `splits[]`,\n * `bestKmPaceSecPerKm` and the elevation chart's x-axis — compute it once.\n */\nexport function cumulativeDistanceSeries(\n  points: { lat: number; lon: number; distanceM?: number }[]\n): { cumDist: number[]; source: 'device' | 'computed' } {\n  if (hasUsableDeviceDistance(points)) {\n    const base = points[0].distanceM!\n    return { cumDist: points.map(p => p.distanceM! - base), source: 'device' }\n  }\n  return { cumDist: cumulativeDistances(points), source: 'computed' }\n}\n","import type { Activity, ActivityStats, Split, HeartRateZones } from './types.js'\nimport { cumulativeDistanceSeries } from './geo.js'\n\n// ---------------------------------------------------------------------------\n// HR zone helpers\n// ---------------------------------------------------------------------------\n\n// Which reference the zone percentage is computed against. Default 'hrmax'\n// reproduces the historical behaviour exactly. 'reserve' uses the Karvonen\n// formula (pct = (hr - restingHR) / (maxHR - restingHR), i.e. % of heart-rate\n// reserve rather than % of HRmax) — a common alternative anchor that shifts\n// low-intensity samples out of z1 relative to %HRmax for the same boundaries.\nexport type HrZoneModel =\n  | { type: 'hrmax'; boundaries?: [number, number, number, number] }\n  | { type: 'reserve'; restingHR: number; boundaries?: [number, number, number, number] }\n\n// Default boundaries reproduce the historical, hardcoded 60/70/80/90% bands\n// (see metrics-spec.md §1.3: a deliberate deviation from Garmin's 50-floor\n// default, kept so z1..z5 always sum to the full elapsed time).\nexport const DEFAULT_ZONE_BOUNDARIES: [number, number, number, number] = [0.6, 0.7, 0.8, 0.9]\n\nfunction validateZoneBoundaries(boundaries: [number, number, number, number]): void {\n  for (const b of boundaries) {\n    if (!(b > 0 && b < 1)) {\n      throw new Error(\n        `Invalid HR zone boundaries [${boundaries.join(', ')}]: each boundary must be strictly between 0 and 1 (got ${b}).`\n      )\n    }\n  }\n  for (let i = 1; i < boundaries.length; i++) {\n    if (!(boundaries[i] > boundaries[i - 1])) {\n      throw new Error(\n        `Invalid HR zone boundaries [${boundaries.join(', ')}]: boundaries must be strictly increasing.`\n      )\n    }\n  }\n}\n\n// Plausible range for a *maximum* heart rate, in bpm — not a formula, just a\n// screen against non-finite/nonsensical caller input (0, negative, NaN, a\n// resting HR typo'd into the maxHR slot). Upper bound mirrors the ubiquitous\n// age-predicted-max estimate (\"220 minus your age\", as published by the\n// American Heart Association's target-heart-rate guidance): at age 0 that\n// formula's own ceiling is 220, and this library has no infant runners.\n// Lower bound is deliberately generous rather than tuned to \"typical\" — it\n// only needs to admit every real maxHR, including heavily beta-blocked or\n// elderly athletes with pronounced chronotropic incompetence during\n// exercise, while still rejecting values with no physiological plausibility\n// at all.\nexport const MIN_PLAUSIBLE_MAX_HR_BPM = 60\nexport const MAX_PLAUSIBLE_MAX_HR_BPM = 220\n\n// Validates the two numbers HR zone percentages are computed *against* —\n// distinct from validateZoneBoundaries, which validates the percentages\n// themselves. Without this, maxHR: 0, NaN, or a negative value, and (for the\n// 'reserve' model) a missing/negative/non-finite restingHR or one >= maxHR,\n// each divide-by-zero or invert the pct formula silently and dump every\n// segment into z1 or z5 — a confident, wrong answer, not a crash. A plain-JS\n// caller gets no type error for an omitted restingHR (HrZoneModel's\n// 'reserve' variant types it as required, but that's a compile-time promise\n// only), so this is the only backstop.\nfunction validateHeartRateInputs(maxHR: number, zoneModel: HrZoneModel): void {\n  if (!Number.isFinite(maxHR) || maxHR < MIN_PLAUSIBLE_MAX_HR_BPM || maxHR > MAX_PLAUSIBLE_MAX_HR_BPM) {\n    throw new Error(\n      `Invalid maxHR: expected a finite number between ${MIN_PLAUSIBLE_MAX_HR_BPM} and ${MAX_PLAUSIBLE_MAX_HR_BPM} bpm (got ${maxHR}).`\n    )\n  }\n  if (zoneModel.type === 'reserve') {\n    const { restingHR } = zoneModel\n    if (restingHR == null || !Number.isFinite(restingHR) || restingHR < 0) {\n      throw new Error(\n        `Invalid restingHR: expected a finite, non-negative number (got ${restingHR}).`\n      )\n    }\n    if (restingHR >= maxHR) {\n      throw new Error(\n        `Invalid restingHR: must be less than maxHR (got restingHR ${restingHR}, maxHR ${maxHR}).`\n      )\n    }\n  }\n}\n\n// Each entry is a segment: `weightSec` is the duration to attribute to the\n// zone of `heartRate` (the segment's *ending* sample — see metrics-spec.md\n// §1.2). A segment with no attributable duration (missing/non-monotonic\n// timestamp) or no ending HR sample must carry `weightSec` / `heartRate` as\n// 0 / undefined respectively so it contributes nothing.\nfunction hrZones(\n  segments: { heartRate?: number; weightSec: number }[],\n  pctOf: (heartRate: number) => number,\n  boundaries: [number, number, number, number]\n): HeartRateZones {\n  const zones: HeartRateZones = { z1: 0, z2: 0, z3: 0, z4: 0, z5: 0 }\n  const [b1, b2, b3, b4] = boundaries\n\n  for (const seg of segments) {\n    if (seg.heartRate == null || seg.weightSec <= 0) continue\n    const pct = pctOf(seg.heartRate)\n    // Deliberate deviation from Garmin's 50/60/70/80/90 model: Garmin's z1\n    // floor is 50% HRmax (below that is \"no zone\"). We have no floor — z1 is\n    // \"everything below the first boundary\" — so that z1..z5 always sum to\n    // the full elapsed time (see spec §1.3/§1.4); a floor would leave\n    // below-floor samples unattributed and break that invariant.\n    if (pct < b1) zones.z1 += seg.weightSec\n    else if (pct < b2) zones.z2 += seg.weightSec\n    else if (pct < b3) zones.z3 += seg.weightSec\n    else if (pct < b4) zones.z4 += seg.weightSec\n    else zones.z5 += seg.weightSec\n  }\n\n  return zones\n}\n\n// ---------------------------------------------------------------------------\n// Elevation hysteresis filter (metrics-spec.md §5)\n// ---------------------------------------------------------------------------\n\n// Fallback path (§5.3 step 2). When a FIT file carries a device-computed\n// session.total_ascent/total_descent, the parser surfaces it as\n// Activity.deviceElevationGainM/LossM and resolveElevation() below prefers\n// it over this filter (§5.3 step 1) — it is what Garmin/Strava will agree\n// with and was filtered on-device. This hysteresis pass still always runs:\n// splits[] need its per-point gains regardless of source, and its totals are\n// the activity figures whenever no trusted device total exists (GPX and TCX\n// always, FIT when the session omits the field).\n\n// Default threshold per spec §5.3: every format this library parses is\n// GPS-derived (GPX always; FIT usually, absent a device total_ascent — see\n// the note above), so the 2-3m *barometric* threshold does not apply here.\n// 8m sits within the GPS-derived band authoritative sources actually\n// recommend (Strava ~10m for non-barometric activities, GPS Visualizer\n// 6-9m). Expose it as a parameter so a caller who knows their source is\n// barometric can lower it toward that 2-3m figure instead.\nexport const DEFAULT_ELEVATION_THRESHOLD_M = 8\n\n// Accumulates confirmed climbs/descents only once the *cumulative* rise from\n// the last confirmed reference point clears `thresholdM`, rejecting\n// oscillation within the noise band. Returns, per point, the confirmed\n// gain/loss amount attributed to that point (its \"ending point\", consistent\n// with the same convention used for HR zones and splits) so callers can slice\n// the totals by distance range without re-running the filter.\nfunction elevationHysteresis(\n  points: { elevation?: number }[],\n  thresholdM: number\n): { gainAtPoint: number[]; lossAtPoint: number[]; totalGainM: number; totalLossM: number } {\n  const gainAtPoint = new Array(points.length).fill(0)\n  const lossAtPoint = new Array(points.length).fill(0)\n  let ref: number | null = null\n  let totalGainM = 0\n  let totalLossM = 0\n\n  for (let i = 0; i < points.length; i++) {\n    const ele = points[i].elevation\n    if (ele == null) continue\n    if (ref == null) { ref = ele; continue }\n\n    const diff = ele - ref\n    if (diff >= thresholdM) {\n      gainAtPoint[i] = diff\n      totalGainM += diff\n      ref = ele\n    } else if (-diff >= thresholdM) {\n      lossAtPoint[i] = -diff\n      totalLossM += -diff\n      ref = ele\n    }\n    // else: within the noise band — ignore, keep ref\n  }\n\n  return { gainAtPoint, lossAtPoint, totalGainM, totalLossM }\n}\n\n// ---------------------------------------------------------------------------\n// Cumulative-series helpers (metrics-spec.md §2.3 / §4.2)\n// ---------------------------------------------------------------------------\n\n// Elapsed time at cumulative distance `x`, linearly interpolated within the\n// segment that contains it. `cumDist`/`cumTime` must be non-decreasing and\n// the same length, with index 0 = the activity's start (0, 0).\nfunction timeAt(x: number, cumDist: number[], cumTime: number[]): number {\n  const n = cumDist.length\n  if (x <= cumDist[0]) return cumTime[0]\n  if (x >= cumDist[n - 1]) return cumTime[n - 1]\n\n  let lo = 1\n  let hi = n - 1\n  while (lo < hi) {\n    const mid = (lo + hi) >> 1\n    if (cumDist[mid] < x) lo = mid + 1\n    else hi = mid\n  }\n  const segLen = cumDist[lo] - cumDist[lo - 1]\n  const f = segLen > 0 ? (x - cumDist[lo - 1]) / segLen : 0\n  return cumTime[lo - 1] + f * (cumTime[lo] - cumTime[lo - 1])\n}\n\n// Fastest 1000m window anywhere in the activity (metrics-spec.md §2), found\n// by scanning the finite candidate set of breakpoints where a window edge\n// coincides with a recorded point — the minimum of the continuous\n// timeAt(s+1000) - timeAt(s) function is always attained at one of these, so\n// a continuous scan isn't needed. Computed from the cumulative series only\n// — independent of splits[] (§2.4) — so it can be faster than (never slower\n// than) the fastest full split, since it isn't quantised to km marks.\nfunction computeBestKmPaceSecPerKm(cumDist: number[], cumTime: number[]): number | null {\n  const total = cumDist[cumDist.length - 1]\n  if (total < 1000) return null\n\n  const candidates = new Set<number>([0, total - 1000])\n  for (const d of cumDist) {\n    if (d + 1000 <= total) candidates.add(d)\n    if (d - 1000 >= 0) candidates.add(d - 1000)\n  }\n\n  let best = Infinity\n  for (const s of candidates) {\n    const windowTime = timeAt(s + 1000, cumDist, cumTime) - timeAt(s, cumDist, cumTime)\n    if (windowTime < best) best = windowTime\n  }\n  return Math.round(best)\n}\n\n// Splits at exact 1000m marks, carrying any overshoot forward instead of\n// resetting at the emitting segment's own (drifting) distance, plus a\n// trailing partial split for any remainder under 1000m (metrics-spec.md §3 +\n// §4). Per-split elevation gain and average HR are attributed to whichever\n// split contains each segment's *ending* point (same convention as HR zones,\n// §1.2), not interpolated — the spec calls exact interpolation there\n// unnecessary precision.\nfunction buildSplits(\n  pts: { heartRate?: number }[],\n  cumDist: number[],\n  cumTime: number[],\n  gainAtPoint: number[]\n): Split[] {\n  const total = cumDist[cumDist.length - 1]\n  const splits: Split[] = []\n\n  let mark = 0\n  let km = 1\n  while (mark + 1000 <= total) {\n    const t0 = timeAt(mark, cumDist, cumTime)\n    const t1 = timeAt(mark + 1000, cumDist, cumTime)\n    splits.push({ km: km++, distanceM: 1000, paceSecPerKm: Math.round(t1 - t0), elevationGainM: 0 })\n    mark += 1000\n  }\n\n  // Trailing partial (§3.2/§3.3): epsilon guards against floating-point dust\n  // emitting a spurious 0m split when total is an exact multiple of 1000.\n  if (total - mark > 1e-6) {\n    const remainderM = total - mark\n    const remainderTimeSec = timeAt(total, cumDist, cumTime) - timeAt(mark, cumDist, cumTime)\n    const pace = remainderTimeSec / (remainderM / 1000)\n    splits.push({ km: km++, distanceM: Math.round(remainderM), paceSecPerKm: Math.round(pace), elevationGainM: 0 })\n  }\n\n  const numSplits = splits.length\n  const hrSum = new Array(numSplits).fill(0)\n  const hrCount = new Array(numSplits).fill(0)\n  const elevSum = new Array(numSplits).fill(0)\n  for (let i = 1; i < cumDist.length; i++) {\n    const idx = Math.ceil(cumDist[i] / 1000) - 1\n    if (idx < 0 || idx >= numSplits) continue\n    elevSum[idx] += gainAtPoint[i]\n    const hr = pts[i].heartRate\n    if (hr != null) { hrSum[idx] += hr; hrCount[idx]++ }\n  }\n  for (let k = 0; k < numSplits; k++) {\n    splits[k].elevationGainM = Math.round(elevSum[k])\n    if (hrCount[k] > 0) splits[k].avgHeartRate = Math.round(hrSum[k] / hrCount[k])\n  }\n\n  return splits\n}\n\n// ---------------------------------------------------------------------------\n// Main analyzer\n// ---------------------------------------------------------------------------\n\n// Guards Activity.deviceDistanceM the same way item 2 guards the per-point\n// device distance stream: a value that isn't plausibly \"this activity's\n// total\" is treated as absent rather than surfaced as-is. A total of 0 means\n// the device didn't record one (most formats omit the field entirely when\n// they have nothing to say, but some emit a zeroed placeholder). A total\n// smaller than the point-stream distance can't be this activity's total\n// either — the device's own total is always at least as large as what was\n// recorded between the first and last point (see Activity.deviceDistanceM\n// for why it's often larger, never smaller, when genuine).\nfunction resolveDeviceDistanceM(raw: number | undefined, pointStreamDistanceM: number): number | undefined {\n  if (raw == null || raw <= 0) return undefined\n  if (raw < pointStreamDistanceM) return undefined\n  return raw\n}\n\n// Resolve the activity's elevation gain/loss and record which source produced\n// them (metrics-spec.md §5.3 step 1 + §5.6). The device's own session-computed\n// total (FIT session.total_ascent/total_descent, surfaced as\n// Activity.deviceElevationGainM/LossM) is preferred when the file carries one:\n// it is barometric/fused, filtered on-device, and the figure Garmin Connect\n// and Strava agree with. Otherwise fall back to the GPS-altitude hysteresis\n// totals passed in.\nfunction resolveElevation(\n  activity: Activity,\n  hystGainM: number,\n  hystLossM: number,\n  hasAltitudeStream: boolean,\n): { gainM: number; lossM: number; source: 'device' | 'computed' } {\n  const deviceGain = activity.deviceElevationGainM\n  if (deviceGain == null) {\n    return { gainM: hystGainM, lossM: hystLossM, source: 'computed' }\n  }\n  // Zero-guard: a device total_ascent of 0 on a track that plainly climbs —\n  // a raw altitude stream is present *and* the hysteresis filter already\n  // found real gain — is a device that never populated the field, not a flat\n  // run, so fall back to the computed figure rather than report a false 0. A\n  // 0 with no altitude stream, or with a hysteresis gain of 0, is a genuinely\n  // flat activity and the device's 0 is honoured (source stays 'device').\n  if (deviceGain === 0 && hasAltitudeStream && hystGainM > 0) {\n    return { gainM: hystGainM, lossM: hystLossM, source: 'computed' }\n  }\n  return {\n    gainM: deviceGain,\n    lossM: activity.deviceElevationLossM ?? 0,\n    source: 'device',\n  }\n}\n\nexport interface AnalyzeOptions {\n  maxHR?: number\n  elevationThresholdM?: number\n  /** Zone model + boundaries for `hrZones`. Default: `{ type: 'hrmax' }` with\n   * boundaries {@link DEFAULT_ZONE_BOUNDARIES}, reproducing historical output. */\n  zoneModel?: HrZoneModel\n  /** Speed (m/s) below which a segment counts as paused, not moving. Default 0.3. */\n  pauseThresholdMps?: number\n}\n\nexport function analyze(activity: Activity, options?: AnalyzeOptions): ActivityStats\n/**\n * @deprecated positional arguments will be removed in 3.0.0; pass an options object\n */\nexport function analyze(activity: Activity, maxHR: number, elevationThresholdM?: number): ActivityStats\nexport function analyze(activity: Activity, arg2?: AnalyzeOptions | number, arg3?: number): ActivityStats {\n  const options: AnalyzeOptions = typeof arg2 === 'number' ? { maxHR: arg2, elevationThresholdM: arg3 } : (arg2 ?? {})\n  const {\n    maxHR = 190,\n    elevationThresholdM = DEFAULT_ELEVATION_THRESHOLD_M,\n    zoneModel = { type: 'hrmax' as const },\n    pauseThresholdMps = 0.3,\n  } = options\n\n  const zoneBoundaries = zoneModel.boundaries ?? DEFAULT_ZONE_BOUNDARIES\n  validateZoneBoundaries(zoneBoundaries)\n  validateHeartRateInputs(maxHR, zoneModel)\n\n  const pctOfMax = zoneModel.type === 'reserve'\n    ? (hr: number) => (hr - zoneModel.restingHR) / (maxHR - zoneModel.restingHR)\n    : (hr: number) => hr / maxHR\n\n  const pts = activity.points\n\n  // Elevation is resolved before the <2-point guard: the device totals are\n  // activity-level scalars, independent of how many points were recorded, so\n  // a sparse track that still carries a session total_ascent should surface\n  // it. The hysteresis pass also produces gainAtPoint, which splits[] needs.\n  const { gainAtPoint, totalGainM: hystGainM, totalLossM: hystLossM } =\n    elevationHysteresis(pts, elevationThresholdM)\n  const hasAltitudeStream = pts.some(p => p.elevation != null)\n  const { gainM: elevationGainM, lossM: elevationLossM, source: elevationSource } =\n    resolveElevation(activity, hystGainM, hystLossM, hasAltitudeStream)\n\n  if (pts.length < 2) {\n    return {\n      distanceM: 0, distanceSource: 'computed',\n      deviceDistanceM: resolveDeviceDistanceM(activity.deviceDistanceM, 0),\n      elapsedTimeSec: 0, movingTimeSec: 0,\n      avgPaceSecPerKm: 0, bestKmPaceSecPerKm: null,\n      elevationGainM: Math.round(elevationGainM), elevationLossM: Math.round(elevationLossM),\n      elevationSource,\n      avgHeartRate: null, maxHeartRate: null, hrZones: null,\n      avgCadence: null, splits: [],\n    }\n  }\n\n  let movingTimeSec = 0\n\n  // Cumulative distance series (metrics-spec.md §2.3), from the device's own\n  // distance stream when the whole track carries a usable one, else summed\n  // haversine. Built once here and reused for splits, best-km and the segment\n  // speeds below — every distance-derived metric shares this one source so\n  // they can never disagree (the elevation chart's x-axis reads it too, via\n  // stats.distanceSource). Per-segment distance is a difference of adjacent\n  // entries, never re-summed a second way.\n  const { cumDist, source: distanceSource } = cumulativeDistanceSeries(pts)\n\n  // Elapsed-time companion series, index 0 = 0, filled in the loop below.\n  const cumTime: number[] = [0]\n\n  // HR\n  const hrValues = pts.map(p => p.heartRate).filter((h): h is number => h != null)\n  const hasHR = hrValues.length > 0\n\n  // HR zone weighting (metrics-spec.md §1): if no point in the whole activity\n  // carries a timestamp, fall back to counting each segment as 1s (§1.5.3).\n  // Otherwise a segment with a missing/non-monotonic timestamp contributes 0s\n  // — it must NOT fall back to 1s, that would re-introduce the count bias\n  // for exactly the corrupt segments (§1.5.2).\n  const noTimestampsAtAll = pts.every(p => p.timestamp == null)\n  const hrZoneSegments: { heartRate?: number; weightSec: number }[] = []\n\n  // Cadence\n  const cadValues = pts.map(p => p.cadence).filter((c): c is number => c != null)\n  const hasCadence = cadValues.length > 0\n\n  for (let i = 1; i < pts.length; i++) {\n    const prev = pts[i - 1]\n    const curr = pts[i]\n\n    // Segment distance is a difference of the shared cumulative series, so it\n    // reflects whichever source won — never a second, independent haversine.\n    const segDist = cumDist[i] - cumDist[i - 1]\n\n    // Time delta\n    let segTimeSec = 1 // default 1s between points if no timestamps\n    if (prev.timestamp && curr.timestamp) {\n      segTimeSec = (curr.timestamp.getTime() - prev.timestamp.getTime()) / 1000\n    }\n    if (segTimeSec <= 0) segTimeSec = 1\n\n    const speedMps = segDist / segTimeSec\n    const isMoving = speedMps > pauseThresholdMps\n\n    if (isMoving) movingTimeSec += segTimeSec\n\n    cumTime.push(cumTime[i - 1] + segTimeSec)\n\n    // HR zone weight for this segment (attributed to curr, the ending sample)\n    let zoneWeightSec: number\n    if (noTimestampsAtAll) {\n      zoneWeightSec = 1\n    } else if (prev.timestamp == null || curr.timestamp == null) {\n      zoneWeightSec = 0\n    } else {\n      const rawDt = (curr.timestamp.getTime() - prev.timestamp.getTime()) / 1000\n      zoneWeightSec = rawDt > 0 ? rawDt : 0\n    }\n    hrZoneSegments.push({ heartRate: curr.heartRate, weightSec: zoneWeightSec })\n  }\n\n  const distanceM = cumDist[cumDist.length - 1]\n  const splits = buildSplits(pts, cumDist, cumTime, gainAtPoint)\n\n  const bestKmPace = computeBestKmPaceSecPerKm(cumDist, cumTime)\n\n  const elapsedTimeSec =\n    pts[0].timestamp && pts[pts.length - 1].timestamp\n      ? (pts[pts.length - 1].timestamp!.getTime() - pts[0].timestamp!.getTime()) / 1000\n      : movingTimeSec\n\n  const avgPaceSecPerKm = distanceM > 0 ? (movingTimeSec / (distanceM / 1000)) : 0\n\n  return {\n    distanceM: Math.round(distanceM),\n    distanceSource,\n    deviceDistanceM: resolveDeviceDistanceM(activity.deviceDistanceM, distanceM),\n    elapsedTimeSec: Math.round(elapsedTimeSec),\n    movingTimeSec: Math.round(movingTimeSec),\n    avgPaceSecPerKm: Math.round(avgPaceSecPerKm),\n    bestKmPaceSecPerKm: bestKmPace != null ? Math.round(bestKmPace) : null,\n    elevationGainM: Math.round(elevationGainM),\n    elevationLossM: Math.round(elevationLossM),\n    elevationSource,\n    avgHeartRate: hasHR ? Math.round(hrValues.reduce((a, b) => a + b, 0) / hrValues.length) : null,\n    // reduce, not Math.max(...hrValues): spreading a long HR array (a multi-hour\n    // activity at 1 Hz is tens of thousands of samples, some FIT files far more)\n    // into a call blows the argument-count/stack limit. avgHeartRate above uses\n    // the same single-pass style.\n    maxHeartRate: hasHR ? hrValues.reduce((m, h) => (h > m ? h : m), -Infinity) : null,\n    hrZones: hasHR ? hrZones(hrZoneSegments, pctOfMax, zoneBoundaries) : null,\n    avgCadence: hasCadence ? Math.round(cadValues.reduce((a, b) => a + b, 0) / cadValues.length) : null,\n    splits,\n  }\n}\n\n// ---------------------------------------------------------------------------\n// Formatting helpers (exported for CLI + reports)\n// ---------------------------------------------------------------------------\n\nexport function formatPace(secPerKm: number, units: 'metric' | 'imperial' = 'metric'): string {\n  const adjusted = units === 'imperial' ? secPerKm * 1.60934 : secPerKm\n  // Round to whole seconds first, then split, so 479.6s → 8:00 (not 7:60).\n  const totalSec = Math.round(adjusted)\n  const min = Math.floor(totalSec / 60)\n  const sec = totalSec % 60\n  const unit = units === 'imperial' ? '/mi' : '/km'\n  return `${min}:${sec.toString().padStart(2, '0')}${unit}`\n}\n\nexport function formatDistance(metres: number, units: 'metric' | 'imperial' = 'metric'): string {\n  if (units === 'imperial') return `${(metres / 1609.34).toFixed(2)} mi`\n  return `${(metres / 1000).toFixed(2)} km`\n}\n\nexport function formatDuration(seconds: number): string {\n  // Round to whole seconds *before* splitting so the carry propagates:\n  // 59.6s → 1:00, not 0:60 (same rule formatPace applies).\n  const totalSec = Math.round(seconds)\n  const h = Math.floor(totalSec / 3600)\n  const m = Math.floor((totalSec % 3600) / 60)\n  const s = totalSec % 60\n  if (h > 0) return `${h}:${m.toString().padStart(2, '0')}:${s.toString().padStart(2, '0')}`\n  return `${m}:${s.toString().padStart(2, '0')}`\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACAA,6BAA0B;AAC1B,oBAAgC;;;ACDhC,gBAAe;AAER,SAAS,cAAc,MAAsB;AAClD,SAAO,UAAAA,QAAG,aAAa,IAAI;AAC7B;;;ADCA,IAAM,YAAY,IAAI,iCAAU;AAAA,EAC9B,kBAAkB;AAAA,EAClB,qBAAqB;AAAA,EACrB,qBAAqB;AACvB,CAAC;AAED,SAAS,QAAW,GAA6B;AAC/C,MAAI,KAAK,KAAM,QAAO,CAAC;AACvB,SAAO,MAAM,QAAQ,CAAC,IAAI,IAAI,CAAC,CAAC;AAClC;AAMA,SAAS,aAAa,GAAgC;AAGpD,MAAI,KAAK,QAAS,OAAO,MAAM,YAAY,EAAE,KAAK,MAAM,GAAK,QAAO;AACpE,QAAM,IAAI,OAAO,CAAC;AAClB,SAAO,OAAO,SAAS,CAAC,IAAI,IAAI;AAClC;AAKA,SAAS,UAAU,GAA8B;AAC/C,QAAM,IAAI,IAAI,KAAK,CAA2B;AAC9C,SAAO,MAAM,EAAE,QAAQ,CAAC,IAAI,SAAY;AAC1C;AAEO,SAAS,SAAS,KAAuB;AAC9C,QAAM,MAAM,UAAU,MAAM,GAAG;AAC/B,QAAM,MAAM,IAAI;AAEhB,QAAM,SAAS,QAAQ,KAAK,GAAG;AAC/B,QAAM,aAAa,OAAO,CAAC;AAE3B,QAAM,OACJ,KAAK,UAAU,QAAQ,YAAY,QAAQ;AAE7C,QAAM,OACJ,YAAY,MAAM,SAAS,EAAE,YAAY,KAAK;AAEhD,QAAM,SAAuB,CAAC;AAE9B,aAAW,OAAO,QAAQ;AACxB,eAAW,OAAO,QAAQ,KAAK,MAAM,GAAG;AACtC,iBAAW,MAAM,QAAQ,KAAK,KAAK,GAAG;AACpC,cAAM,MAAM,WAAW,GAAG,OAAO,CAAC;AAClC,cAAM,MAAM,WAAW,GAAG,OAAO,CAAC;AAClC,YAAI,MAAM,GAAG,KAAK,MAAM,GAAG,EAAG;AAE9B,cAAM,QAAoB,EAAE,KAAK,IAAI;AAErC,cAAM,MAAM,aAAa,GAAG,GAAG;AAC/B,YAAI,GAAG,OAAO,QAAQ,OAAO,KAAM,OAAM,YAAY;AACrD,YAAI,GAAG,KAAM,OAAM,YAAY,UAAU,GAAG,IAAI;AAGhD,cAAM,MAAM,GAAG;AACf,YAAI,KAAK;AACP,gBAAM,MACJ,IAAI,4BAA4B,KAChC,IAAI,yBAAyB,KAC7B,IAAI;AAEN,cAAI,KAAK;AACP,kBAAM,KACJ,IAAI,WAAW,KAAK,IAAI,QAAQ,KAAK,IAAI;AAC3C,kBAAM,MACJ,IAAI,YAAY,KAAK,IAAI,SAAS,KAAK,IAAI;AAE7C,kBAAM,QAAQ,aAAa,EAAE;AAC7B,gBAAI,SAAS,KAAM,OAAM,YAAY,KAAK,MAAM,KAAK;AACrD,kBAAM,SAAS,aAAa,GAAG;AAC/B,gBAAI,UAAU,KAAM,OAAM,UAAU,KAAK,MAAM,MAAM,IAAI;AAAA,UAC3D;AAAA,QACF;AAEA,eAAO,KAAK,KAAK;AAAA,MACnB;AAAA,IACF;AAAA,EACF;AAEA,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA,WAAW,OAAO,CAAC,GAAG;AAAA,IACtB;AAAA,IACA,QAAQ;AAAA,EACV;AACF;AAMO,SAAS,SAAS,KAAuB;AAC9C,QAAM,MAAM,UAAU,MAAM,GAAG;AAC/B,QAAM,aAAa,QAAQ,KAAK,wBAAwB,YAAY,QAAQ;AAC5E,QAAM,QAAQ,WAAW,CAAC;AAE1B,QAAM,OACJ,QAAQ,SAAS,GAAG,SAAS,EAAE,YAAY,KAAK;AAElD,QAAM,SAAuB,CAAC;AAK9B,MAAI;AAEJ,aAAW,YAAY,YAAY;AACjC,eAAW,OAAO,QAAQ,UAAU,GAAG,GAAG;AACxC,YAAM,UAAU,OAAO,KAAK,cAAc;AAC1C,UAAI,CAAC,MAAM,OAAO,EAAG,oBAAmB,mBAAmB,KAAK;AAEhE,iBAAW,SAAS,QAAQ,KAAK,KAAK,GAAG;AACvC,mBAAW,MAAM,QAAQ,OAAO,UAAU,GAAG;AAC3C,gBAAM,MAAM,IAAI;AAChB,cAAI,CAAC,IAAK;AAEV,gBAAM,MAAM,OAAO,IAAI,eAAe;AACtC,gBAAM,MAAM,OAAO,IAAI,gBAAgB;AACvC,cAAI,MAAM,GAAG,KAAK,MAAM,GAAG,EAAG;AAE9B,gBAAM,QAAoB,EAAE,KAAK,IAAI;AAErC,gBAAM,SAAS,aAAa,GAAG,cAAc;AAC7C,cAAI,GAAG,kBAAkB,QAAQ,UAAU,KAAM,OAAM,YAAY;AACnE,cAAI,GAAG,KAAM,OAAM,YAAY,UAAU,GAAG,IAAI;AAGhD,cAAI,GAAG,kBAAkB,MAAM;AAC7B,kBAAM,IAAI,OAAO,GAAG,cAAc;AAClC,gBAAI,CAAC,MAAM,CAAC,EAAG,OAAM,YAAY;AAAA,UACnC;AAGA,gBAAM,KAAK,aAAa,GAAG,cAAc,SAAS,GAAG,YAAY;AACjE,cAAI,MAAM,KAAM,OAAM,YAAY;AAIlC,gBAAM,MAAM,GAAG;AACf,cAAI,KAAK;AACP,kBAAM,MAAM,IAAI,SAAS,KAAK,IAAI;AAClC,kBAAM,MAAM,aAAa,MAAM,gBAAgB,KAAK,KAAK,UAAU;AAEnE,gBAAI,OAAO,KAAM,OAAM,UAAU,KAAK,MAAM,MAAM,CAAC;AAAA,UACrD;AAEA,iBAAO,KAAK,KAAK;AAAA,QACnB;AAAA,MACF;AAAA,IACF;AAAA,EACF;AAEA,QAAM,aAAa,OAAO,KAAK,UAAU,MAAM,EAAE,IAAI,WAAc,OAAO,CAAC,GAAG;AAE9E,SAAO,EAAE,MAAM,WAAW,QAAQ,QAAQ,OAAO,gBAAgB;AACnE;AAMA,IAAM,oBAAoB,MAAM,KAAK;AAE9B,SAAS,QAAQ,OAAiE;AACvF,MAAI,iBAAiB,WAAY,QAAO;AACxC,MAAI,iBAAiB,YAAa,QAAO,IAAI,WAAW,KAAK;AAC7D,SAAO,WAAW,KAAK,KAA0B;AACnD;AAGO,SAAS,MAAM,OAA4B;AAChD,MAAI;AACF,WAAO,sBAAQ,MAAM,qBAAO,cAAc,KAAK,CAAC;AAAA,EAClD,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAEO,SAAS,SAAS,OAA6B;AACpD,QAAM,SAAS,qBAAO,cAAc,KAAK;AACzC,MAAI,CAAC,sBAAQ,MAAM,MAAM,GAAG;AAC1B,UAAM,IAAI,MAAM,4DAA4D;AAAA,EAC9E;AAEA,QAAM,UAAU,IAAI,sBAAQ,MAAM;AAElC,QAAM,EAAE,SAAS,IAAI,QAAQ,KAAK;AAElC,QAAM,UAAqC,SAAS,eAAe,CAAC;AACpE,QAAM,SAAuB,CAAC;AAE9B,aAAW,KAAK,SAAS;AACvB,UAAM,SAAS,EAAE;AACjB,UAAM,SAAS,EAAE;AACjB,QAAI,UAAU,QAAQ,UAAU,KAAM;AAEtC,UAAM,MAAM,SAAS;AACrB,UAAM,MAAM,SAAS;AACrB,QAAI,MAAM,GAAG,KAAK,MAAM,GAAG,EAAG;AAE9B,UAAM,QAAoB,EAAE,KAAK,IAAI;AAGrC,UAAM,MAAO,EAAE,oBAAoB,EAAE;AACrC,QAAI,OAAO,KAAM,OAAM,YAAY;AAInC,UAAM,OAAQ,EAAE,oBAAoB,EAAE;AACtC,QAAI,QAAQ,KAAM,OAAM,YAAY;AAEpC,QAAI,EAAE,aAAa,KAAM,OAAM,YAAY,IAAI,KAAK,EAAE,SAAmC;AAEzF,UAAM,KAAK,EAAE;AACb,QAAI,MAAM,KAAM,OAAM,YAAY;AAIlC,UAAM,MAAM,EAAE;AACd,QAAI,OAAO,MAAM;AACf,YAAM,OAAQ,EAAE,qBAA4C;AAC5D,YAAM,UAAU,KAAK,OAAO,MAAM,QAAQ,CAAC;AAAA,IAC7C;AAEA,WAAO,KAAK,KAAK;AAAA,EACnB;AAEA,QAAM,QAAS,SAAS,aAAa,CAAC,KAAK,CAAC;AAC5C,QAAM,WAAY,SAAS,gBAAgB,CAAC;AAC5C,QAAM,UAAW,SAAS,CAAC,KAAK,CAAC;AAEjC,QAAM,OAAQ,MAAM,QAA+B;AACnD,QAAM,QAAS,MAAM,SAAS,QAAQ,QAA+B,SAAS,EAAE,YAAY;AAC5F,QAAM,YAAY,QAAQ,YACtB,IAAI,KAAK,QAAQ,SAAmC,IACpD,OAAO,CAAC,GAAG;AAMf,QAAM,mBAAmB,SACtB,IAAI,OAAK,EAAE,aAAmC,EAC9C,OAAO,CAAC,MAAmB,KAAK,QAAQ,CAAC,MAAM,CAAC,CAAC;AACpD,QAAM,kBAAkB,iBAAiB,SAAS,IAC9C,iBAAiB,OAAO,CAAC,GAAG,MAAM,IAAI,GAAG,CAAC,IAC1C;AAQJ,QAAM,kBAAkB,CAAC,UAA8D;AACrF,UAAM,SAAS,SACZ,IAAI,OAAK,EAAE,KAAK,CAAuB,EACvC,OAAO,CAAC,MAAmB,KAAK,QAAQ,CAAC,MAAM,CAAC,CAAC;AACpD,WAAO,OAAO,SAAS,IAAI,OAAO,OAAO,CAAC,GAAG,MAAM,IAAI,GAAG,CAAC,IAAI;AAAA,EACjE;AACA,QAAM,uBAAuB,gBAAgB,aAAa;AAC1D,QAAM,uBAAuB,gBAAgB,cAAc;AAE3D,SAAO;AAAA,IACL;AAAA,IAAM;AAAA,IAAM;AAAA,IAAW;AAAA,IAAQ,QAAQ;AAAA,IACvC;AAAA,IAAiB;AAAA,IAAsB;AAAA,EACzC;AACF;AAOO,SAAS,UAAU,MAAwB;AAChD,MAAI,KAAK,SAAS,yBAAyB,EAAG,QAAO,SAAS,IAAI;AAClE,MAAI,KAAK,SAAS,MAAM,EAAG,QAAO,SAAS,IAAI;AAC/C,QAAM,IAAI,MAAM,4DAA4D;AAC9E;AAOA,SAAS,SAAS,GAAW,MAAM,IAAY;AAC7C,SAAO,EAAE,SAAS,MAAM,GAAG,EAAE,MAAM,GAAG,GAAG,CAAC,WAAM;AAClD;AAEA,SAAS,SAAS,KAA4C;AAC5D,SAAO,OAAO,QAAQ,YAAY,QAAQ,QAAS,IAA8B,SAAS;AAC5F;AAaO,SAAS,MAAM,OAA0C,SAAkC;AAEhG,MAAI,OAAO,UAAU,UAAU;AAC7B,WAAO,SAAS,QAAQ,KAAK,CAAC;AAAA,EAChC;AAGA,MAAI,SAAS,WAAW,MAAO,QAAO,SAAS,KAAK;AACpD,MAAI,SAAS,WAAW,MAAO,QAAO,SAAS,KAAK;AACpD,MAAI,SAAS,WAAW,MAAO,QAAO,SAAS,QAAQ,OAAO,KAAK,OAAO,QAAQ,CAAC,CAAC;AAGpF,QAAM,SAAS,CAAC,MAAM,UAAU,EAAE,WAAW,GAAG,KAAK,MAAM,SAAS;AACpE,MAAI,QAAQ;AACV,QAAI;AACJ,QAAI;AACF,YAAM,cAAc,KAAK;AAAA,IAC3B,SAAS,KAAK;AACZ,UAAI,SAAS,GAAG,GAAG;AACjB,cAAM,IAAI;AAAA,UACR,YAAY,SAAS,KAAK,CAAC;AAAA,QAC7B;AAAA,MACF;AACA,YAAM;AAAA,IACR;AACA,UAAM,QAAQ,QAAQ,GAAG;AACzB,QAAI,MAAM,KAAK,EAAG,QAAO,SAAS,KAAK;AACvC,WAAO,UAAU,IAAI,SAAS,OAAO,CAAC;AAAA,EACxC;AAGA,SAAO,UAAU,KAAK;AACxB;;;AE5VA,sBAAyB;AAezB,eAAsB,UAAU,MAAc,SAA+C;AAC3F,QAAM,MAAM,UAAM,0BAAS,IAAI;AAE/B,MAAI,SAAS,WAAW,MAAO,QAAO,SAAS,IAAI,SAAS,OAAO,CAAC;AACpE,MAAI,SAAS,WAAW,MAAO,QAAO,SAAS,IAAI,SAAS,OAAO,CAAC;AACpE,MAAI,SAAS,WAAW,MAAO,QAAO,SAAS,QAAQ,GAAG,CAAC;AAE3D,QAAM,QAAQ,QAAQ,GAAG;AACzB,MAAI,MAAM,KAAK,EAAG,QAAO,SAAS,KAAK;AACvC,SAAO,UAAU,IAAI,SAAS,OAAO,CAAC;AACxC;;;ACpBA,IAAM,IAAI;AAEH,SAAS,UAAU,GAAiC,GAAyC;AAClG,QAAM,QAAQ,CAAC,MAAe,IAAI,KAAK,KAAM;AAC7C,QAAM,OAAO,MAAM,EAAE,MAAM,EAAE,GAAG;AAChC,QAAM,OAAO,MAAM,EAAE,MAAM,EAAE,GAAG;AAChC,QAAM,UAAU,KAAK,IAAI,OAAO,CAAC;AACjC,QAAM,UAAU,KAAK,IAAI,OAAO,CAAC;AACjC,QAAM,IACJ,UAAU,UACV,KAAK,IAAI,MAAM,EAAE,GAAG,CAAC,IAAI,KAAK,IAAI,MAAM,EAAE,GAAG,CAAC,IAAI,UAAU;AAC9D,SAAO,IAAI,IAAI,KAAK,MAAM,KAAK,KAAK,CAAC,GAAG,KAAK,KAAK,IAAI,CAAC,CAAC;AAC1D;AAWO,SAAS,oBAAoB,QAAkD;AACpF,QAAM,UAAU,IAAI,MAAM,OAAO,MAAM,EAAE,KAAK,CAAC;AAC/C,WAAS,IAAI,GAAG,IAAI,OAAO,QAAQ,KAAK;AACtC,YAAQ,CAAC,IAAI,QAAQ,IAAI,CAAC,IAAI,UAAU,OAAO,IAAI,CAAC,GAAG,OAAO,CAAC,CAAC;AAAA,EAClE;AACA,SAAO;AACT;AAWO,SAAS,wBAAwB,QAA2C;AACjF,MAAI,OAAO,WAAW,EAAG,QAAO;AAChC,MAAI,OAAO;AACX,MAAI,MAAM;AACV,aAAW,KAAK,QAAQ;AACtB,UAAM,IAAI,EAAE;AACZ,QAAI,KAAK,QAAQ,CAAC,OAAO,SAAS,CAAC,EAAG,QAAO;AAC7C,QAAI,IAAI,KAAM,QAAO;AACrB,WAAO;AACP,QAAI,IAAI,IAAK,OAAM;AAAA,EACrB;AACA,SAAO,MAAM;AACf;AAcO,SAAS,yBACd,QACsD;AACtD,MAAI,wBAAwB,MAAM,GAAG;AACnC,UAAM,OAAO,OAAO,CAAC,EAAE;AACvB,WAAO,EAAE,SAAS,OAAO,IAAI,OAAK,EAAE,YAAa,IAAI,GAAG,QAAQ,SAAS;AAAA,EAC3E;AACA,SAAO,EAAE,SAAS,oBAAoB,MAAM,GAAG,QAAQ,WAAW;AACpE;;;AC5DO,IAAM,0BAA4D,CAAC,KAAK,KAAK,KAAK,GAAG;AAE5F,SAAS,uBAAuB,YAAoD;AAClF,aAAW,KAAK,YAAY;AAC1B,QAAI,EAAE,IAAI,KAAK,IAAI,IAAI;AACrB,YAAM,IAAI;AAAA,QACR,+BAA+B,WAAW,KAAK,IAAI,CAAC,0DAA0D,CAAC;AAAA,MACjH;AAAA,IACF;AAAA,EACF;AACA,WAAS,IAAI,GAAG,IAAI,WAAW,QAAQ,KAAK;AAC1C,QAAI,EAAE,WAAW,CAAC,IAAI,WAAW,IAAI,CAAC,IAAI;AACxC,YAAM,IAAI;AAAA,QACR,+BAA+B,WAAW,KAAK,IAAI,CAAC;AAAA,MACtD;AAAA,IACF;AAAA,EACF;AACF;AAaO,IAAM,2BAA2B;AACjC,IAAM,2BAA2B;AAWxC,SAAS,wBAAwB,OAAe,WAA8B;AAC5E,MAAI,CAAC,OAAO,SAAS,KAAK,KAAK,QAAQ,4BAA4B,QAAQ,0BAA0B;AACnG,UAAM,IAAI;AAAA,MACR,mDAAmD,wBAAwB,QAAQ,wBAAwB,aAAa,KAAK;AAAA,IAC/H;AAAA,EACF;AACA,MAAI,UAAU,SAAS,WAAW;AAChC,UAAM,EAAE,UAAU,IAAI;AACtB,QAAI,aAAa,QAAQ,CAAC,OAAO,SAAS,SAAS,KAAK,YAAY,GAAG;AACrE,YAAM,IAAI;AAAA,QACR,kEAAkE,SAAS;AAAA,MAC7E;AAAA,IACF;AACA,QAAI,aAAa,OAAO;AACtB,YAAM,IAAI;AAAA,QACR,6DAA6D,SAAS,WAAW,KAAK;AAAA,MACxF;AAAA,IACF;AAAA,EACF;AACF;AAOA,SAAS,QACP,UACA,OACA,YACgB;AAChB,QAAM,QAAwB,EAAE,IAAI,GAAG,IAAI,GAAG,IAAI,GAAG,IAAI,GAAG,IAAI,EAAE;AAClE,QAAM,CAAC,IAAI,IAAI,IAAI,EAAE,IAAI;AAEzB,aAAW,OAAO,UAAU;AAC1B,QAAI,IAAI,aAAa,QAAQ,IAAI,aAAa,EAAG;AACjD,UAAM,MAAM,MAAM,IAAI,SAAS;AAM/B,QAAI,MAAM,GAAI,OAAM,MAAM,IAAI;AAAA,aACrB,MAAM,GAAI,OAAM,MAAM,IAAI;AAAA,aAC1B,MAAM,GAAI,OAAM,MAAM,IAAI;AAAA,aAC1B,MAAM,GAAI,OAAM,MAAM,IAAI;AAAA,QAC9B,OAAM,MAAM,IAAI;AAAA,EACvB;AAEA,SAAO;AACT;AAsBO,IAAM,gCAAgC;AAQ7C,SAAS,oBACP,QACA,YAC0F;AAC1F,QAAM,cAAc,IAAI,MAAM,OAAO,MAAM,EAAE,KAAK,CAAC;AACnD,QAAM,cAAc,IAAI,MAAM,OAAO,MAAM,EAAE,KAAK,CAAC;AACnD,MAAI,MAAqB;AACzB,MAAI,aAAa;AACjB,MAAI,aAAa;AAEjB,WAAS,IAAI,GAAG,IAAI,OAAO,QAAQ,KAAK;AACtC,UAAM,MAAM,OAAO,CAAC,EAAE;AACtB,QAAI,OAAO,KAAM;AACjB,QAAI,OAAO,MAAM;AAAE,YAAM;AAAK;AAAA,IAAS;AAEvC,UAAM,OAAO,MAAM;AACnB,QAAI,QAAQ,YAAY;AACtB,kBAAY,CAAC,IAAI;AACjB,oBAAc;AACd,YAAM;AAAA,IACR,WAAW,CAAC,QAAQ,YAAY;AAC9B,kBAAY,CAAC,IAAI,CAAC;AAClB,oBAAc,CAAC;AACf,YAAM;AAAA,IACR;AAAA,EAEF;AAEA,SAAO,EAAE,aAAa,aAAa,YAAY,WAAW;AAC5D;AASA,SAAS,OAAO,GAAW,SAAmB,SAA2B;AACvE,QAAM,IAAI,QAAQ;AAClB,MAAI,KAAK,QAAQ,CAAC,EAAG,QAAO,QAAQ,CAAC;AACrC,MAAI,KAAK,QAAQ,IAAI,CAAC,EAAG,QAAO,QAAQ,IAAI,CAAC;AAE7C,MAAI,KAAK;AACT,MAAI,KAAK,IAAI;AACb,SAAO,KAAK,IAAI;AACd,UAAM,MAAO,KAAK,MAAO;AACzB,QAAI,QAAQ,GAAG,IAAI,EAAG,MAAK,MAAM;AAAA,QAC5B,MAAK;AAAA,EACZ;AACA,QAAM,SAAS,QAAQ,EAAE,IAAI,QAAQ,KAAK,CAAC;AAC3C,QAAM,IAAI,SAAS,KAAK,IAAI,QAAQ,KAAK,CAAC,KAAK,SAAS;AACxD,SAAO,QAAQ,KAAK,CAAC,IAAI,KAAK,QAAQ,EAAE,IAAI,QAAQ,KAAK,CAAC;AAC5D;AASA,SAAS,0BAA0B,SAAmB,SAAkC;AACtF,QAAM,QAAQ,QAAQ,QAAQ,SAAS,CAAC;AACxC,MAAI,QAAQ,IAAM,QAAO;AAEzB,QAAM,aAAa,oBAAI,IAAY,CAAC,GAAG,QAAQ,GAAI,CAAC;AACpD,aAAW,KAAK,SAAS;AACvB,QAAI,IAAI,OAAQ,MAAO,YAAW,IAAI,CAAC;AACvC,QAAI,IAAI,OAAQ,EAAG,YAAW,IAAI,IAAI,GAAI;AAAA,EAC5C;AAEA,MAAI,OAAO;AACX,aAAW,KAAK,YAAY;AAC1B,UAAM,aAAa,OAAO,IAAI,KAAM,SAAS,OAAO,IAAI,OAAO,GAAG,SAAS,OAAO;AAClF,QAAI,aAAa,KAAM,QAAO;AAAA,EAChC;AACA,SAAO,KAAK,MAAM,IAAI;AACxB;AASA,SAAS,YACP,KACA,SACA,SACA,aACS;AACT,QAAM,QAAQ,QAAQ,QAAQ,SAAS,CAAC;AACxC,QAAM,SAAkB,CAAC;AAEzB,MAAI,OAAO;AACX,MAAI,KAAK;AACT,SAAO,OAAO,OAAQ,OAAO;AAC3B,UAAM,KAAK,OAAO,MAAM,SAAS,OAAO;AACxC,UAAM,KAAK,OAAO,OAAO,KAAM,SAAS,OAAO;AAC/C,WAAO,KAAK,EAAE,IAAI,MAAM,WAAW,KAAM,cAAc,KAAK,MAAM,KAAK,EAAE,GAAG,gBAAgB,EAAE,CAAC;AAC/F,YAAQ;AAAA,EACV;AAIA,MAAI,QAAQ,OAAO,MAAM;AACvB,UAAM,aAAa,QAAQ;AAC3B,UAAM,mBAAmB,OAAO,OAAO,SAAS,OAAO,IAAI,OAAO,MAAM,SAAS,OAAO;AACxF,UAAM,OAAO,oBAAoB,aAAa;AAC9C,WAAO,KAAK,EAAE,IAAI,MAAM,WAAW,KAAK,MAAM,UAAU,GAAG,cAAc,KAAK,MAAM,IAAI,GAAG,gBAAgB,EAAE,CAAC;AAAA,EAChH;AAEA,QAAM,YAAY,OAAO;AACzB,QAAM,QAAQ,IAAI,MAAM,SAAS,EAAE,KAAK,CAAC;AACzC,QAAM,UAAU,IAAI,MAAM,SAAS,EAAE,KAAK,CAAC;AAC3C,QAAM,UAAU,IAAI,MAAM,SAAS,EAAE,KAAK,CAAC;AAC3C,WAAS,IAAI,GAAG,IAAI,QAAQ,QAAQ,KAAK;AACvC,UAAM,MAAM,KAAK,KAAK,QAAQ,CAAC,IAAI,GAAI,IAAI;AAC3C,QAAI,MAAM,KAAK,OAAO,UAAW;AACjC,YAAQ,GAAG,KAAK,YAAY,CAAC;AAC7B,UAAM,KAAK,IAAI,CAAC,EAAE;AAClB,QAAI,MAAM,MAAM;AAAE,YAAM,GAAG,KAAK;AAAI,cAAQ,GAAG;AAAA,IAAI;AAAA,EACrD;AACA,WAAS,IAAI,GAAG,IAAI,WAAW,KAAK;AAClC,WAAO,CAAC,EAAE,iBAAiB,KAAK,MAAM,QAAQ,CAAC,CAAC;AAChD,QAAI,QAAQ,CAAC,IAAI,EAAG,QAAO,CAAC,EAAE,eAAe,KAAK,MAAM,MAAM,CAAC,IAAI,QAAQ,CAAC,CAAC;AAAA,EAC/E;AAEA,SAAO;AACT;AAeA,SAAS,uBAAuB,KAAyB,sBAAkD;AACzG,MAAI,OAAO,QAAQ,OAAO,EAAG,QAAO;AACpC,MAAI,MAAM,qBAAsB,QAAO;AACvC,SAAO;AACT;AASA,SAAS,iBACP,UACA,WACA,WACA,mBACiE;AACjE,QAAM,aAAa,SAAS;AAC5B,MAAI,cAAc,MAAM;AACtB,WAAO,EAAE,OAAO,WAAW,OAAO,WAAW,QAAQ,WAAW;AAAA,EAClE;AAOA,MAAI,eAAe,KAAK,qBAAqB,YAAY,GAAG;AAC1D,WAAO,EAAE,OAAO,WAAW,OAAO,WAAW,QAAQ,WAAW;AAAA,EAClE;AACA,SAAO;AAAA,IACL,OAAO;AAAA,IACP,OAAO,SAAS,wBAAwB;AAAA,IACxC,QAAQ;AAAA,EACV;AACF;AAiBO,SAAS,QAAQ,UAAoB,MAAgC,MAA8B;AACxG,QAAM,UAA0B,OAAO,SAAS,WAAW,EAAE,OAAO,MAAM,qBAAqB,KAAK,IAAK,QAAQ,CAAC;AAClH,QAAM;AAAA,IACJ,QAAQ;AAAA,IACR,sBAAsB;AAAA,IACtB,YAAY,EAAE,MAAM,QAAiB;AAAA,IACrC,oBAAoB;AAAA,EACtB,IAAI;AAEJ,QAAM,iBAAiB,UAAU,cAAc;AAC/C,yBAAuB,cAAc;AACrC,0BAAwB,OAAO,SAAS;AAExC,QAAM,WAAW,UAAU,SAAS,YAChC,CAAC,QAAgB,KAAK,UAAU,cAAc,QAAQ,UAAU,aAChE,CAAC,OAAe,KAAK;AAEzB,QAAM,MAAM,SAAS;AAMrB,QAAM,EAAE,aAAa,YAAY,WAAW,YAAY,UAAU,IAChE,oBAAoB,KAAK,mBAAmB;AAC9C,QAAM,oBAAoB,IAAI,KAAK,OAAK,EAAE,aAAa,IAAI;AAC3D,QAAM,EAAE,OAAO,gBAAgB,OAAO,gBAAgB,QAAQ,gBAAgB,IAC5E,iBAAiB,UAAU,WAAW,WAAW,iBAAiB;AAEpE,MAAI,IAAI,SAAS,GAAG;AAClB,WAAO;AAAA,MACL,WAAW;AAAA,MAAG,gBAAgB;AAAA,MAC9B,iBAAiB,uBAAuB,SAAS,iBAAiB,CAAC;AAAA,MACnE,gBAAgB;AAAA,MAAG,eAAe;AAAA,MAClC,iBAAiB;AAAA,MAAG,oBAAoB;AAAA,MACxC,gBAAgB,KAAK,MAAM,cAAc;AAAA,MAAG,gBAAgB,KAAK,MAAM,cAAc;AAAA,MACrF;AAAA,MACA,cAAc;AAAA,MAAM,cAAc;AAAA,MAAM,SAAS;AAAA,MACjD,YAAY;AAAA,MAAM,QAAQ,CAAC;AAAA,IAC7B;AAAA,EACF;AAEA,MAAI,gBAAgB;AASpB,QAAM,EAAE,SAAS,QAAQ,eAAe,IAAI,yBAAyB,GAAG;AAGxE,QAAM,UAAoB,CAAC,CAAC;AAG5B,QAAM,WAAW,IAAI,IAAI,OAAK,EAAE,SAAS,EAAE,OAAO,CAAC,MAAmB,KAAK,IAAI;AAC/E,QAAM,QAAQ,SAAS,SAAS;AAOhC,QAAM,oBAAoB,IAAI,MAAM,OAAK,EAAE,aAAa,IAAI;AAC5D,QAAM,iBAA8D,CAAC;AAGrE,QAAM,YAAY,IAAI,IAAI,OAAK,EAAE,OAAO,EAAE,OAAO,CAAC,MAAmB,KAAK,IAAI;AAC9E,QAAM,aAAa,UAAU,SAAS;AAEtC,WAAS,IAAI,GAAG,IAAI,IAAI,QAAQ,KAAK;AACnC,UAAM,OAAO,IAAI,IAAI,CAAC;AACtB,UAAM,OAAO,IAAI,CAAC;AAIlB,UAAM,UAAU,QAAQ,CAAC,IAAI,QAAQ,IAAI,CAAC;AAG1C,QAAI,aAAa;AACjB,QAAI,KAAK,aAAa,KAAK,WAAW;AACpC,oBAAc,KAAK,UAAU,QAAQ,IAAI,KAAK,UAAU,QAAQ,KAAK;AAAA,IACvE;AACA,QAAI,cAAc,EAAG,cAAa;AAElC,UAAM,WAAW,UAAU;AAC3B,UAAM,WAAW,WAAW;AAE5B,QAAI,SAAU,kBAAiB;AAE/B,YAAQ,KAAK,QAAQ,IAAI,CAAC,IAAI,UAAU;AAGxC,QAAI;AACJ,QAAI,mBAAmB;AACrB,sBAAgB;AAAA,IAClB,WAAW,KAAK,aAAa,QAAQ,KAAK,aAAa,MAAM;AAC3D,sBAAgB;AAAA,IAClB,OAAO;AACL,YAAM,SAAS,KAAK,UAAU,QAAQ,IAAI,KAAK,UAAU,QAAQ,KAAK;AACtE,sBAAgB,QAAQ,IAAI,QAAQ;AAAA,IACtC;AACA,mBAAe,KAAK,EAAE,WAAW,KAAK,WAAW,WAAW,cAAc,CAAC;AAAA,EAC7E;AAEA,QAAM,YAAY,QAAQ,QAAQ,SAAS,CAAC;AAC5C,QAAM,SAAS,YAAY,KAAK,SAAS,SAAS,WAAW;AAE7D,QAAM,aAAa,0BAA0B,SAAS,OAAO;AAE7D,QAAM,iBACJ,IAAI,CAAC,EAAE,aAAa,IAAI,IAAI,SAAS,CAAC,EAAE,aACnC,IAAI,IAAI,SAAS,CAAC,EAAE,UAAW,QAAQ,IAAI,IAAI,CAAC,EAAE,UAAW,QAAQ,KAAK,MAC3E;AAEN,QAAM,kBAAkB,YAAY,IAAK,iBAAiB,YAAY,OAAS;AAE/E,SAAO;AAAA,IACL,WAAW,KAAK,MAAM,SAAS;AAAA,IAC/B;AAAA,IACA,iBAAiB,uBAAuB,SAAS,iBAAiB,SAAS;AAAA,IAC3E,gBAAgB,KAAK,MAAM,cAAc;AAAA,IACzC,eAAe,KAAK,MAAM,aAAa;AAAA,IACvC,iBAAiB,KAAK,MAAM,eAAe;AAAA,IAC3C,oBAAoB,cAAc,OAAO,KAAK,MAAM,UAAU,IAAI;AAAA,IAClE,gBAAgB,KAAK,MAAM,cAAc;AAAA,IACzC,gBAAgB,KAAK,MAAM,cAAc;AAAA,IACzC;AAAA,IACA,cAAc,QAAQ,KAAK,MAAM,SAAS,OAAO,CAAC,GAAG,MAAM,IAAI,GAAG,CAAC,IAAI,SAAS,MAAM,IAAI;AAAA;AAAA;AAAA;AAAA;AAAA,IAK1F,cAAc,QAAQ,SAAS,OAAO,CAAC,GAAG,MAAO,IAAI,IAAI,IAAI,GAAI,SAAS,IAAI;AAAA,IAC9E,SAAS,QAAQ,QAAQ,gBAAgB,UAAU,cAAc,IAAI;AAAA,IACrE,YAAY,aAAa,KAAK,MAAM,UAAU,OAAO,CAAC,GAAG,MAAM,IAAI,GAAG,CAAC,IAAI,UAAU,MAAM,IAAI;AAAA,IAC/F;AAAA,EACF;AACF;AAMO,SAAS,WAAW,UAAkB,QAA+B,UAAkB;AAC5F,QAAM,WAAW,UAAU,aAAa,WAAW,UAAU;AAE7D,QAAM,WAAW,KAAK,MAAM,QAAQ;AACpC,QAAM,MAAM,KAAK,MAAM,WAAW,EAAE;AACpC,QAAM,MAAM,WAAW;AACvB,QAAM,OAAO,UAAU,aAAa,QAAQ;AAC5C,SAAO,GAAG,GAAG,IAAI,IAAI,SAAS,EAAE,SAAS,GAAG,GAAG,CAAC,GAAG,IAAI;AACzD;AAEO,SAAS,eAAe,QAAgB,QAA+B,UAAkB;AAC9F,MAAI,UAAU,WAAY,QAAO,IAAI,SAAS,SAAS,QAAQ,CAAC,CAAC;AACjE,SAAO,IAAI,SAAS,KAAM,QAAQ,CAAC,CAAC;AACtC;AAEO,SAAS,eAAe,SAAyB;AAGtD,QAAM,WAAW,KAAK,MAAM,OAAO;AACnC,QAAM,IAAI,KAAK,MAAM,WAAW,IAAI;AACpC,QAAM,IAAI,KAAK,MAAO,WAAW,OAAQ,EAAE;AAC3C,QAAM,IAAI,WAAW;AACrB,MAAI,IAAI,EAAG,QAAO,GAAG,CAAC,IAAI,EAAE,SAAS,EAAE,SAAS,GAAG,GAAG,CAAC,IAAI,EAAE,SAAS,EAAE,SAAS,GAAG,GAAG,CAAC;AACxF,SAAO,GAAG,CAAC,IAAI,EAAE,SAAS,EAAE,SAAS,GAAG,GAAG,CAAC;AAC9C;","names":["fs"]}