> ## Documentation Index
> Fetch the complete documentation index at: https://docs.canadava.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Toronto Pearson (CYYZ)

> vACA briefing for CYYZ Toronto Pearson: runway layout, ATC frequencies, common procedures, hot spots, live METAR, and online ATC network coverage

export const Gates = ({sections = [], title = "Gates", terminal, labels = {}}) => {
  const DEFAULT_LABELS = {
    range: "Range",
    operators: "Operators",
    aircraft: "Aircraft"
  };
  const mergedLabels = {
    ...DEFAULT_LABELS,
    ...labels
  };
  const typeKey = type => {
    const t = (type || "").toLowerCase();
    if (t.includes("domestic") || t.includes("domestique")) return "domestic";
    if (t.includes("transborder") || t.includes("transfrontalier") || t.includes("us")) return "transborder";
    if (t.includes("international")) return "international";
    if (t.includes("regional") || t.includes("régional")) return "regional";
    if (t.includes("remote") || t.includes("hardstand") || t.includes("isol")) return "remote";
    if (t.includes("swing") || t.includes("polyvalent")) return "swing";
    if (t.includes("deice") || t.includes("dégivrage") || t.includes("ice")) return "deice";
    return "default";
  };
  const TypeIcon = ({k}) => {
    const svgProps = {
      width: 14,
      height: 14,
      viewBox: "0 0 24 24",
      fill: "none",
      stroke: "currentColor",
      strokeWidth: 2,
      strokeLinecap: "round",
      strokeLinejoin: "round",
      "aria-hidden": true
    };
    switch (k) {
      case "domestic":
        return <svg {...svgProps}>
            <path d="m3 9 9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z" />
            <polyline points="9 22 9 12 15 12 15 22" />
          </svg>;
      case "international":
        return <svg {...svgProps}>
            <circle cx="12" cy="12" r="10" />
            <path d="M12 2a14.5 14.5 0 0 0 0 20 14.5 14.5 0 0 0 0-20" />
            <path d="M2 12h20" />
          </svg>;
      case "transborder":
        return <svg {...svgProps}>
            <path d="M4 15s1-1 4-1 5 2 8 2 4-1 4-1V3s-1 1-4 1-5-2-8-2-4 1-4 1z" />
            <line x1="4" x2="4" y1="22" y2="15" />
          </svg>;
      case "regional":
        return <svg {...svgProps}>
            <path d="M17.8 19.2 16 11l3.5-3.5C21 6 21.5 4 21 3c-1-.5-3 0-4.5 1.5L13 8 4.8 6.2c-.5-.1-.9.1-1.1.5l-.3.5c-.2.5-.1 1 .3 1.3L9 12l-2 3H4l-1 1 3 2 2 3 1-1v-3l3-2 3.5 5.3c.3.4.8.5 1.3.3l.5-.2c.4-.3.6-.7.5-1.2z" />
          </svg>;
      case "remote":
        return <svg {...svgProps}>
            <path d="M11 21.73a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73z" />
            <path d="M12 22V12" />
            <polyline points="3.29 7 12 12 20.71 7" />
            <path d="m7.5 4.27 9 5.15" />
          </svg>;
      case "swing":
        return <svg {...svgProps}>
            <path d="M8 3 4 7l4 4" />
            <path d="M4 7h16" />
            <path d="m16 21 4-4-4-4" />
            <path d="M20 17H4" />
          </svg>;
      case "deice":
        return <svg {...svgProps}>
            <line x1="2" x2="22" y1="12" y2="12" />
            <line x1="12" x2="12" y1="2" y2="22" />
            <path d="m20 16-4-4 4-4" />
            <path d="m4 8 4 4-4 4" />
            <path d="m16 4-4 4-4-4" />
            <path d="m8 20 4-4 4 4" />
          </svg>;
      default:
        return <svg {...svgProps}>
            <rect x="3" y="3" width="18" height="18" rx="2" />
          </svg>;
    }
  };
  const Chip = ({value, variant}) => <span className={`kb-gates-chip kb-gates-chip--${variant}`}>
      <span className="kb-gates-chip-value">{value}</span>
    </span>;
  return <div className="kb-gates">
      <div className="kb-gates-header">
        <span className="kb-gates-eyebrow">{title}</span>
        {terminal && <span className="kb-gates-station">
            <span className="kb-gates-station-terminal">{terminal}</span>
          </span>}
      </div>
      <div className="kb-gates-list">
        {sections.map((section, i) => {
    const k = typeKey(section.type);
    const operators = Array.isArray(section.operators) ? section.operators : [];
    const aircraft = Array.isArray(section.aircraft) ? section.aircraft : [];
    const gates = Array.isArray(section.gates) ? section.gates : [];
    return <div key={`g-${i}`} className="kb-gates-row" data-type={k}>
              <div className="kb-gates-type">
                <span className="kb-gates-type-icon" aria-hidden="true">
                  <TypeIcon k={k} />
                </span>
                <div className="kb-gates-type-text">
                  <div className="kb-gates-type-label">{section.label}</div>
                  {section.subLabel && <div className="kb-gates-type-note">{section.subLabel}</div>}
                </div>
              </div>
              <div className="kb-gates-body">
                {section.range && <div className="kb-gates-range">
                    <span className="kb-gates-range-value">{section.range}</span>
                  </div>}
                {gates.length > 0 && <div className="kb-gates-chips kb-gates-chips--gates">
                    {gates.map((g, j) => <Chip key={`gx-${i}-${j}`} value={g} variant="gate" />)}
                  </div>}
                {operators.length > 0 && <div className="kb-gates-meta">
                    <span className="kb-gates-meta-label">{mergedLabels.operators}</span>
                    <div className="kb-gates-chips">
                      {operators.map((o, j) => <Chip key={`op-${i}-${j}`} value={o} variant="operator" />)}
                    </div>
                  </div>}
                {aircraft.length > 0 && <div className="kb-gates-meta">
                    <span className="kb-gates-meta-label">{mergedLabels.aircraft}</span>
                    <div className="kb-gates-chips">
                      {aircraft.map((a, j) => <Chip key={`ac-${i}-${j}`} value={a} variant="aircraft" />)}
                    </div>
                  </div>}
                {section.notes && <div className="kb-gates-notes">{section.notes}</div>}
              </div>
            </div>;
  })}
      </div>
    </div>;
};

export const ChartsLinks = ({icao, title = "Charts", freeLabel = "Free"}) => {
  const code = (icao || "").toUpperCase().trim();
  const providers = [{
    name: "ChartFox",
    href: code ? `https://chartfox.org/${code}` : "https://chartfox.org",
    logoSrc: "https://chartfox.org/images/icons/180x180.png",
    badge: freeLabel
  }, {
    name: "Navigraph",
    href: code ? `https://charts.navigraph.com/airport/${code}` : "https://charts.navigraph.com",
    logoSrc: "https://charts.navigraph.com/favicon.ico"
  }];
  return <div className="kb-charts-links">
      <div className="kb-charts-links-header">
        <span className="kb-charts-links-eyebrow">{title}</span>
      </div>
      <div className="kb-charts-links-grid">
        {providers.map(({name, href, logoSrc, badge}) => <a key={name} className="kb-charts-link" href={href} target="_blank" rel="noopener noreferrer">
            <span className="kb-charts-link-logo" aria-hidden="true">
              <img src={logoSrc} alt="" width="20" height="20" loading="lazy" />
            </span>
            <span className="kb-charts-link-name">{name}</span>
            {badge && <span className="kb-charts-link-badge">{badge}</span>}
            <span className="kb-charts-link-chevron" aria-hidden="true">
              <svg width="14" height="14" viewBox="0 0 14 14" fill="none">
                <path d="M5 3L9 7L5 11" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
              </svg>
            </span>
          </a>)}
      </div>
    </div>;
};

export const NetworkLinks = ({icao, title = "Online ATC Network Maps"}) => {
  const code = (icao || "").toUpperCase();
  const networks = [{
    name: "VATSIM",
    href: `https://vatsim-radar.com/airport/${code}`,
    logoSrc: "/images/networks/vatsim.png"
  }, {
    name: "IVAO",
    href: `https://webeye.ivao.aero/?airportId=${code}`,
    logoSrc: "/images/networks/ivao.svg"
  }, {
    name: "POSCON",
    href: "https://hq.poscon.net/map",
    logoSrc: "/images/networks/poscon.ico"
  }, {
    name: "PilotEdge",
    href: "http://map.pilotedge.net/map/",
    logoSrc: "/images/networks/pilotedge.ico"
  }];
  return <div className="kb-network-links">
      <div className="kb-network-links-header">
        <span className="kb-network-links-eyebrow">{title}</span>
      </div>
      <div className="kb-network-links-grid">
        {networks.map(({name, href, logoSrc}) => <a key={name} className="kb-network-link" href={href} target="_blank" rel="noopener noreferrer">
            <span className="kb-network-link-logo" aria-hidden="true">
              <img src={logoSrc} alt="" width="20" height="20" loading="lazy" />
            </span>
            <span className="kb-network-link-name">{name}</span>
            <span className="kb-network-link-chevron" aria-hidden="true">
              <svg width="14" height="14" viewBox="0 0 14 14" fill="none">
                <path d="M5 3L9 7L5 11" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
              </svg>
            </span>
          </a>)}
      </div>
    </div>;
};

export const AtcFrequencies = ({frequencies = [], title = "ATC Frequencies"}) => {
  const roleKey = role => {
    const r = (role || "").toLowerCase();
    if (r.includes("atis")) return "atis";
    if (r.includes("clearance") || r.includes("autorisation") || r.includes("clnc")) return "clearance";
    if (r.includes("sol") || r.includes("ground")) return "ground";
    if (r.includes("tower") || r.includes("tour")) return "tower";
    if (r.includes("départ") || r.includes("depart")) return "departure";
    if (r.includes("arrivée") || r.includes("arrival")) return "arrival";
    if (r.includes("terminal") || r.includes("approach") || r.includes("approche")) return "terminal";
    if (r.includes("center") || r.includes("centre") || r.includes("enroute")) return "center";
    if (r.includes("vfr") || r.includes("advisory") || r.includes("conseil")) return "advisory";
    return "default";
  };
  const RoleIcon = ({k}) => {
    const stroke = {
      stroke: "currentColor",
      strokeWidth: 1.4,
      strokeLinecap: "round",
      strokeLinejoin: "round",
      fill: "none"
    };
    switch (k) {
      case "atis":
        return <svg width="14" height="14" viewBox="0 0 16 16" aria-hidden="true">
            <path d="M3 7v2M5.5 5.5v5M8 4v8" {...stroke} />
            <path d="M10.5 6.5C11.6 7 12 7.5 12 8s-0.4 1-1.5 1.5" {...stroke} />
            <path d="M12 5C13.7 5.7 14.5 6.7 14.5 8s-0.8 2.3-2.5 3" {...stroke} />
          </svg>;
      case "clearance":
        return <svg width="14" height="14" viewBox="0 0 16 16" aria-hidden="true">
            <rect x="3.5" y="2.5" width="9" height="11" rx="1" {...stroke} />
            <path d="M6 6h4M6 8.5h4M6 11h2.5" {...stroke} />
            <path d="M9.5 11l1 1 2-2.2" {...stroke} stroke="#10b981" strokeWidth="1.5" />
          </svg>;
      case "ground":
        return <svg width="14" height="14" viewBox="0 0 16 16" aria-hidden="true">
            <path d="M2.5 12.5h11" {...stroke} />
            <path d="M5 9.5L8 12.5L11 9.5" {...stroke} />
            <path d="M8 4v8.5" {...stroke} />
            <circle cx="3" cy="12.5" r="0.5" fill="currentColor" />
            <circle cx="13" cy="12.5" r="0.5" fill="currentColor" />
          </svg>;
      case "tower":
        return <svg width="14" height="14" viewBox="0 0 16 16" aria-hidden="true">
            <path d="M6 13.5V8l-1.2-1V5.5L8 4l3.2 1.5V7L10 8v5.5" {...stroke} />
            <path d="M4.5 13.5h7" {...stroke} />
            <path d="M8 4V2.5" {...stroke} />
            <path d="M6 6.5h4" {...stroke} />
            <path d="M2.5 3.5l1 -1M13.5 3.5l-1 -1" {...stroke} />
          </svg>;
      case "departure":
        return <svg width="14" height="14" viewBox="0 0 16 16" aria-hidden="true">
            <path d="M2.5 13.5h11" {...stroke} />
            <path d="M2.5 11l9-4.5 c1.4-0.7 2.5 0.4 1.8 1.8L8 12 M5.5 10.2L4.2 8" {...stroke} />
          </svg>;
      case "arrival":
        return <svg width="14" height="14" viewBox="0 0 16 16" aria-hidden="true">
            <path d="M2.5 13.5h11" {...stroke} />
            <path d="M13 11.5L4 7c-1.4-0.7-2.5 0.4-1.8 1.8L7.5 12.5z" {...stroke} />
          </svg>;
      case "terminal":
        return <svg width="14" height="14" viewBox="0 0 16 16" aria-hidden="true">
            <path d="M8 13.5V8" {...stroke} />
            <path d="M3 11.5a6 6 0 0 1 10 0" {...stroke} />
            <path d="M5 9a4 4 0 0 1 6 0" {...stroke} />
            <circle cx="8" cy="8" r="0.8" fill="currentColor" />
          </svg>;
      case "center":
        return <svg width="14" height="14" viewBox="0 0 16 16" aria-hidden="true">
            <circle cx="8" cy="8" r="5.5" {...stroke} />
            <path d="M8 2.5L9 8L8 13.5L7 8Z" fill="currentColor" />
            <circle cx="8" cy="8" r="0.8" fill="currentColor" />
          </svg>;
      case "advisory":
        return <svg width="14" height="14" viewBox="0 0 16 16" aria-hidden="true">
            <circle cx="8" cy="8" r="5.5" {...stroke} />
            <path d="M8 5v3.5M8 11h0.01" {...stroke} />
          </svg>;
      default:
        return <svg width="14" height="14" viewBox="0 0 16 16" aria-hidden="true">
            <circle cx="8" cy="8" r="5.5" {...stroke} />
          </svg>;
    }
  };
  return <div className="kb-atc-freqs">
      <div className="kb-atc-freqs-header">
        <span className="kb-atc-freqs-eyebrow">{title}</span>
      </div>
      <div className="kb-atc-freqs-list">
        {frequencies.map((row, i) => {
    const k = roleKey(row.role);
    const freqs = (row.freqs || []).map(f => typeof f === "string" ? {
      value: f
    } : f);
    return <div key={`r-${i}`} className="kb-atc-freq-row" data-role={k}>
              <div className="kb-atc-freq-role">
                <span className="kb-atc-freq-role-icon" aria-hidden="true">
                  <RoleIcon k={k} />
                </span>
                <div className="kb-atc-freq-role-text">
                  <div className="kb-atc-freq-role-label">{row.role}</div>
                  {row.subLabel && <div className="kb-atc-freq-role-note">{row.subLabel}</div>}
                </div>
              </div>
              <div className="kb-atc-freq-chips">
                {freqs.map((f, j) => <div key={`${i}-${j}-${f.value}`} className="kb-atc-freq-chip">
                    <span className="kb-atc-freq-chip-value">{f.value}</span>
                    {f.note && <span className="kb-atc-freq-chip-note">{f.note}</span>}
                  </div>)}
              </div>
            </div>;
  })}
      </div>
    </div>;
};

export const Runways = ({runways = [], magneticVariation, title = "Runways", labels = {}}) => {
  const ilsClass = cat => {
    if (!cat) return "kb-runway-pill--no-ils";
    const c = String(cat).toUpperCase();
    if (c.includes("III")) return "kb-runway-pill--cat3";
    if (c.includes("II")) return "kb-runway-pill--cat2";
    return "kb-runway-pill--cat1";
  };
  const fmt = n => typeof n === "number" ? n.toLocaleString("en-US") : n;
  const DEFAULT_LABELS = {
    length: "Length",
    width: "Width",
    surface: "Surface",
    lighting: "Lighting",
    noIls: "None",
    ilsPrefix: "ILS",
    magvar: "Magnetic variation"
  };
  const mergedLabels = {
    ...DEFAULT_LABELS,
    ...labels
  };
  return <div className="kb-runways">
      <div className="kb-runways-header">
        <span className="kb-runways-eyebrow">{title}</span>
        {magneticVariation && <span className="kb-runways-variation">
            <svg width="11" height="11" viewBox="0 0 12 12" fill="none" aria-hidden="true">
              <circle cx="6" cy="6" r="5" stroke="currentColor" strokeWidth="1" opacity="0.6" />
              <path d="M6 1.5L6 4M6 10.5L6 8" stroke="currentColor" strokeWidth="1" strokeLinecap="round" opacity="0.6" />
              <path d="M6 3L4 7H8L6 3Z" fill="currentColor" />
            </svg>
            {mergedLabels.magvar} {magneticVariation}
          </span>}
      </div>

      <div className="kb-runways-list">
        {runways.map((runway, index) => {
    const [d1, d2] = runway.designators;
    const ilsEntries = runway.ils ? Object.entries(runway.ils) : [];
    return <div key={`${d1}-${d2}`} className="kb-runway" style={{
      "--i": index
    }}>
              <div className="kb-runway-strip">
                <svg viewBox="0 0 1200 150" xmlns="http://www.w3.org/2000/svg" preserveAspectRatio="xMidYMid meet" role="img" aria-label={`Runway ${d1}/${d2}`}>
                  <rect x="0" y="20" width="1200" height="110" rx="5" className="kb-runway-body" />

                  {[0, 1, 2, 3, 4, 5].map(i => <rect key={`pl-${i}`} x={20 + i * 12} y="36" width="7" height="78" fill="white" opacity="0.92" rx="1" />)}
                  {[0, 1, 2, 3, 4, 5].map(i => <rect key={`pr-${i}`} x={1180 - i * 12 - 7} y="36" width="7" height="78" fill="white" opacity="0.92" rx="1" />)}

                  {[...Array(18)].map((_, i) => <rect key={`cl-${i}`} x={240 + i * 40} y="71" width="24" height="8" fill="white" opacity="0.88" rx="1" />)}

                  <text x="142" y="75" fontSize="46" fontWeight="700" fill="white" textAnchor="middle" dominantBaseline="middle" style={{
      fontFamily: "ui-monospace, 'SF Mono', 'Geist Mono', Menlo, monospace",
      letterSpacing: "1.5px"
    }}>
                    {d1}
                  </text>
                  <text x="1058" y="75" fontSize="46" fontWeight="700" fill="white" textAnchor="middle" dominantBaseline="middle" style={{
      fontFamily: "ui-monospace, 'SF Mono', 'Geist Mono', Menlo, monospace",
      letterSpacing: "1.5px"
    }}>
                    {d2}
                  </text>
                </svg>
              </div>

              <div className="kb-runway-scale">
                <span>{fmt(runway.length_ft)} ft</span>
                <span>{fmt(runway.length_m)} m</span>
              </div>

              <div className="kb-runway-meta">
                <span className="kb-runway-pill">
                  <span className="kb-runway-pill-label">{mergedLabels.length}</span>
                  <span className="kb-runway-pill-value">
                    {fmt(runway.length_ft)} ft · {fmt(runway.length_m)} m
                  </span>
                </span>
                {runway.width_ft && <span className="kb-runway-pill">
                    <span className="kb-runway-pill-label">{mergedLabels.width}</span>
                    <span className="kb-runway-pill-value">{runway.width_ft} ft</span>
                  </span>}
                {runway.surface && <span className="kb-runway-pill">
                    <span className="kb-runway-pill-label">{mergedLabels.surface}</span>
                    <span className="kb-runway-pill-value">{runway.surface}</span>
                  </span>}
                {ilsEntries.length > 0 ? ilsEntries.map(([threshold, cat]) => <span key={threshold} className={`kb-runway-pill ${ilsClass(cat)}`}>
                      <span className="kb-runway-pill-label">{threshold}</span>
                      <span className="kb-runway-pill-value">
                        {cat ? `${mergedLabels.ilsPrefix} ${cat}` : mergedLabels.noIls}
                      </span>
                    </span>) : <span className="kb-runway-pill kb-runway-pill--no-ils">
                    <span className="kb-runway-pill-label">{mergedLabels.ilsPrefix}</span>
                    <span className="kb-runway-pill-value">{mergedLabels.noIls}</span>
                  </span>}
                {runway.lighting && <span className="kb-runway-pill">
                    <span className="kb-runway-pill-label">{mergedLabels.lighting}</span>
                    <span className="kb-runway-pill-value">{runway.lighting}</span>
                  </span>}
              </div>

              {runway.notes && <div className="kb-runway-notes">{runway.notes}</div>}
            </div>;
  })}
      </div>
    </div>;
};

export const VatsimAtis = ({icao, name, runways = [], magneticVariation, title = "VATSIM ATIS", loadingLabel = "Tuning broadcast", offlineLabel = "No live VATSIM ATIS for this airport right now", errorLabel = "Unable to reach VATSIM data feed", refreshLabel = "Refresh", infoLabel = "Info", frequencyLabel = "Frequency", observedLabel = "Updated", agoLabel = "min ago", justNowLabel = "just now", windLabel = "Wind", altimeterLabel = "Altimeter", approachLabel = "Approach", transcriptLabel = "Transcript", runwaysInUseLabel = "Runways in use", notUsedLabel = "Not in use", depLabel = "DEP", arrLabel = "ARR", bearingLabel = "Bearing", runwayLabel = "Runway", calmLabel = "Calm", variableLabel = "Variable", gustingLabel = "gusting", fromLabel = "from", atLabel = "at", arrivalsBroadcastLabel = "Arrivals", departuresBroadcastLabel = "Departures"}) => {
  const code = (icao || "").toUpperCase();
  const [data, setData] = useState(null);
  const [metarWind, setMetarWind] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);
  const [refreshing, setRefreshing] = useState(false);
  const [nowTick, setNowTick] = useState(Date.now());
  const parseMagVar = mv => {
    if (!mv) return 0;
    const m = String(mv).match(/(\d+(?:\.\d+)?)\s*°?\s*([EW])/i);
    if (!m) return 0;
    return parseFloat(m[1]) * (m[2].toUpperCase() === "E" ? 1 : -1);
  };
  const magVarDeg = parseMagVar(magneticVariation);
  const parseEmbeddedMetar = text => {
    if (!text) return {
      wind: null,
      altimeter: null
    };
    const ups = text.toUpperCase();
    let wind = null;
    const wm = ups.match(/\b(VRB|\d{3})(\d{2,3})(?:G(\d{2,3}))?\s*(KT|MPS|KMH)\b/);
    if (wm) {
      wind = {
        dir: wm[1] === "VRB" ? "VRB" : +wm[1],
        speed: +wm[2],
        gust: wm[3] ? +wm[3] : null,
        unit: wm[4]
      };
    } else if ((/\b00000(KT|MPS|KMH)\b/).test(ups)) {
      wind = {
        calm: true
      };
    }
    let altimeter = null;
    const altA = ups.match(/\bA(\d{4})\b/);
    const altQ = ups.match(/\bQ(\d{4})\b/);
    if (altA) altimeter = {
      value: +altA[1] / 100,
      unit: "inHg"
    }; else if (altQ) altimeter = {
      value: +altQ[1],
      unit: "hPa"
    };
    return {
      wind,
      altimeter
    };
  };
  const parseRunwaysInUse = text => {
    if (!text) return {
      departures: [],
      arrivals: [],
      approachType: null
    };
    const ups = text.toUpperCase();
    const deps = new Set();
    const arrs = new Set();
    let approachType = null;
    const clauses = ups.split(/(?:\.{1,}|;|\s-\s)/);
    const runwayInClauseRe = /\b(?:RUNWAYS|RUNWAY|RWYS|RWY)\s*((?:\d{1,2}\s*(?:LEFT|RIGHT|CENTER|CENTRE|L|R|C)?)(?:\s*(?:,|\/|AND|&)\s*\d{1,2}\s*(?:LEFT|RIGHT|CENTER|CENTRE|L|R|C)?)*)/g;
    for (const clause of clauses) {
      if ((/\b(OTS|U\/S|UNSERVICEABLE|OUT\s+OF\s+SERVICE|CLOSED)\b/).test(clause)) continue;
      const isDep = (/\b(DEPARTURE|DEPARTURES|DEPARTING|DEPTG|DEPG|TAKEOFF|TKOF|TAKE\s*OFF|DEPS|FOR\s+DEP)\b/).test(clause);
      const isArr = (/\b(APPROACH|APPROACHES|APCH|APCHS|APPR|ARRIVAL|ARRIVALS|ARRIVING|LANDING|LDG|FOR\s+LANDING|FOR\s+ARRIVAL)\b/).test(clause);
      const inUse = (/\b(IN\s+USE|ACTIVE)\b/).test(clause);
      if (!isDep && !isArr && !inUse) continue;
      const ap = clause.match(/\b(ILS|RNAV|VOR|VISUAL|LOC|RNP|GPS)\b/);
      if (ap && !approachType) approachType = ap[1];
      for (const m of clause.matchAll(runwayInClauseRe)) {
        const normalized = m[1].replace(/\s*LEFT\b/g, "L").replace(/\s*RIGHT\b/g, "R").replace(/\s*(?:CENTER|CENTRE)\b/g, "C").split(/[,\s\/&]+|AND/).filter(Boolean);
        for (const d of normalized) {
          const mm = d.match(/^(\d{1,2})([LRC])?$/);
          if (!mm) continue;
          const num = parseInt(mm[1], 10);
          if (num < 1 || num > 36) continue;
          const norm = String(num).padStart(2, "0") + (mm[2] || "");
          if (isDep) deps.add(norm);
          if (isArr) arrs.add(norm);
          if (!isDep && !isArr && inUse) {
            deps.add(norm);
            arrs.add(norm);
          }
        }
      }
    }
    return {
      departures: Array.from(deps),
      arrivals: Array.from(arrs),
      approachType
    };
  };
  const windComponent = (windDirTrue, windSpeed, runwayBearingMag) => {
    if (windDirTrue == null || windSpeed == null) return null;
    const windDirMag = ((windDirTrue - magVarDeg) % 360 + 360) % 360;
    let angle = windDirMag - runwayBearingMag;
    while (angle > 180) angle -= 360;
    while (angle < -180) angle += 360;
    const rad = angle * Math.PI / 180;
    return {
      headwind: windSpeed * Math.cos(rad),
      crosswind: windSpeed * Math.sin(rad),
      relAngle: angle,
      speed: windSpeed
    };
  };
  const runwayEnds = [];
  runways.forEach(r => {
    const [d1, d2] = r.designators || [];
    const bearings = r.bearings || [];
    if (d1) {
      const b = bearings[0] != null ? bearings[0] : (parseInt(d1, 10) || 0) * 10;
      runwayEnds.push({
        designator: d1,
        bearing: b
      });
    }
    if (d2) {
      const b = bearings[1] != null ? bearings[1] : (parseInt(d2, 10) || 0) * 10;
      runwayEnds.push({
        designator: d2,
        bearing: b
      });
    }
  });
  const fetchAtis = async isUserRefresh => {
    try {
      if (isUserRefresh) setRefreshing(true); else setLoading(true);
      setError(null);
      const res = await fetch("https://data.vatsim.net/v3/vatsim-data.json", {
        cache: "no-store"
      });
      if (!res.ok) throw new Error("http");
      const json = await res.json();
      const all = Array.isArray(json.atis) ? json.atis : [];
      const re = new RegExp(`^${code}(?:_[AD])?_ATIS$`, "i");
      const matches = all.filter(a => re.test(a.callsign || "")).map(a => {
        const text = Array.isArray(a.text_atis) ? a.text_atis.join(" ") : "";
        const cs = (a.callsign || "").toUpperCase();
        const split = cs.includes("_A_ATIS") ? "arrival" : cs.includes("_D_ATIS") ? "departure" : null;
        return {
          callsign: a.callsign,
          atisCode: a.atis_code || "",
          frequency: a.frequency,
          text,
          lastUpdated: a.last_updated,
          split,
          ...parseEmbeddedMetar(text),
          ...parseRunwaysInUse(text)
        };
      });
      const merged = matches.reduce((acc, m) => {
        m.departures.forEach(r => acc.departures.add(r));
        m.arrivals.forEach(r => acc.arrivals.add(r));
        return acc;
      }, {
        departures: new Set(),
        arrivals: new Set()
      });
      setData({
        broadcasts: matches,
        departures: Array.from(merged.departures),
        arrivals: Array.from(merged.arrivals)
      });
    } catch (e) {
      setError("fetch");
    } finally {
      setLoading(false);
      if (isUserRefresh) {
        setTimeout(() => setRefreshing(false), 600);
      }
    }
  };
  useEffect(() => {
    if (!code) return;
    fetchAtis(false);
    const interval = setInterval(() => fetchAtis(false), 60 * 1000);
    return () => clearInterval(interval);
  }, [code]);
  useEffect(() => {
    if (!code) return;
    let cancelled = false;
    const fetchMetar = async () => {
      try {
        const res = await fetch(`https://metar.vatsim.net/${code}`, {
          cache: "no-store"
        });
        if (!res.ok) return;
        const text = (await res.text()).trim();
        if (cancelled || !text) return;
        const parsed = parseEmbeddedMetar(text);
        setMetarWind(parsed.wind || null);
      } catch (_) {}
    };
    fetchMetar();
    const interval = setInterval(fetchMetar, 5 * 60 * 1000);
    return () => {
      cancelled = true;
      clearInterval(interval);
    };
  }, [code]);
  useEffect(() => {
    const tick = setInterval(() => setNowTick(Date.now()), 30000);
    return () => clearInterval(tick);
  }, []);
  const primary = data?.broadcasts?.[0] || null;
  const wind = primary?.wind || metarWind || null;
  const altimeter = primary?.altimeter || null;
  const departures = data?.departures || [];
  const arrivals = data?.arrivals || [];
  const isOnline = data && data.broadcasts && data.broadcasts.length > 0;
  const mostRecentUpdate = isOnline ? data.broadcasts.map(b => b.lastUpdated).filter(Boolean).sort().pop() : null;
  const formatFreq = f => {
    if (!f) return "---.---";
    const n = parseFloat(f);
    if (Number.isNaN(n)) return f;
    return n.toFixed(3);
  };
  const formatWind = w => {
    if (!w) return "—";
    if (w.calm) return calmLabel;
    const dir = w.dir === "VRB" ? variableLabel : `${String(w.dir).padStart(3, "0")}°`;
    let s = `${fromLabel} ${dir} ${atLabel} ${w.speed} kt`;
    if (w.gust) s += `, ${gustingLabel} ${w.gust} kt`;
    return s;
  };
  const formatAltimeter = a => {
    if (!a) return "—";
    if (a.unit === "inHg") {
      return `${a.value.toFixed(2)} inHg · ${Math.round(a.value * 33.8639)} hPa`;
    }
    return `${a.value} hPa · ${(a.value / 33.8639).toFixed(2)} inHg`;
  };
  const ageLabel = iso => {
    if (!iso) return null;
    const t = Date.parse(iso);
    if (Number.isNaN(t)) return null;
    const diffMin = Math.max(0, Math.round((nowTick - t) / 60000));
    if (diffMin === 0) return justNowLabel;
    return `${diffMin} ${agoLabel}`;
  };
  const endsVm = runwayEnds.map(end => {
    const desig = end.designator.toUpperCase();
    const isDep = departures.includes(desig);
    const isArr = arrivals.includes(desig);
    const status = !isOnline ? "unknown" : isDep || isArr ? "active" : "not_used";
    let comp = null;
    if (wind && !wind.calm && typeof wind.dir === "number") {
      const speed = wind.gust || wind.speed;
      comp = windComponent(wind.dir, speed, end.bearing);
    }
    return {
      ...end,
      desig,
      isDep,
      isArr,
      status,
      comp
    };
  });
  const renderRunwayRow = (vm, index) => {
    const isUnknown = vm.status === "unknown";
    const isNotUsed = vm.status === "not_used";
    let windChips = null;
    if (vm.comp) {
      const speed = Math.round(vm.comp.speed);
      const hw = Math.round(vm.comp.headwind);
      const cw = Math.round(vm.comp.crosswind);
      const hwAbs = Math.abs(hw);
      const cwAbs = Math.abs(cw);
      let crossLevel = "green";
      if (cwAbs > 25) crossLevel = "red"; else if (cwAbs > 15) crossLevel = "amber";
      const longitudinalLevel = hw < 0 ? "red" : "green";
      const crossArrowPath = cw >= 0 ? "M10 6 H3 M6 3 L3 6 L6 9" : "M2 6 H9 M6 3 L9 6 L6 9";
      const longArrowPath = hw >= 0 ? "M6 2 V9 M3 6 L6 9 L9 6" : "M6 10 V3 M3 6 L6 3 L9 6";
      const cwSideLabel = cw >= 0 ? "from the right" : "from the left";
      const longTypeLabel = hw >= 0 ? "headwind" : "tailwind";
      const title = `RWY ${vm.desig} (bearing ${String(Math.round(vm.bearing)).padStart(3, "0")}°): ${cwAbs} kt crosswind ${cwSideLabel}, ${hwAbs} kt ${longTypeLabel}. Total wind ${speed} kt.`;
      windChips = <span className="kb-vatsim-atis-wind-chip" title={title}>
          <span className={`kb-vatsim-atis-wind-row kb-vatsim-atis-wind-row--${crossLevel}`}>
            <svg width="10" height="10" viewBox="0 0 12 12" aria-hidden="true">
              <path d={crossArrowPath} stroke="currentColor" strokeWidth="1.6" fill="none" strokeLinecap="round" strokeLinejoin="round" />
            </svg>
            <span>{cwAbs} kt</span>
          </span>
          <span className={`kb-vatsim-atis-wind-row kb-vatsim-atis-wind-row--${longitudinalLevel}`}>
            <svg width="10" height="10" viewBox="0 0 12 12" aria-hidden="true">
              <path d={longArrowPath} stroke="currentColor" strokeWidth="1.6" fill="none" strokeLinecap="round" strokeLinejoin="round" />
            </svg>
            <span>{hwAbs} kt</span>
          </span>
        </span>;
    }
    return <div key={`${vm.desig}-${index}`} className={`kb-vatsim-atis-rwy kb-vatsim-atis-rwy--${vm.status}`} style={{
      "--i": index
    }}>
        <div className="kb-vatsim-atis-rwy-label">
          <span className="kb-vatsim-atis-rwy-label-eyebrow">{runwayLabel}</span>
          <span className="kb-vatsim-atis-rwy-designator">{vm.desig}</span>
        </div>

        <div className="kb-vatsim-atis-rwy-wind">
          {windChips || <span className="kb-vatsim-atis-rwy-wind-empty">—</span>}
        </div>

        <div className="kb-vatsim-atis-rwy-usage">
          {isNotUsed ? <span className="kb-vatsim-atis-usage-pill kb-vatsim-atis-usage-pill--off">
              {notUsedLabel}
            </span> : isUnknown ? <span className="kb-vatsim-atis-usage-pill kb-vatsim-atis-usage-pill--off">—</span> : <span className="kb-vatsim-atis-usage-group">
              <span className={`kb-vatsim-atis-usage-pill${vm.isDep ? "" : " kb-vatsim-atis-usage-pill--dim"}`}>
                {depLabel}
              </span>
              <span className={`kb-vatsim-atis-usage-pill${vm.isArr ? "" : " kb-vatsim-atis-usage-pill--dim"}`}>
                {arrLabel}
              </span>
            </span>}
        </div>

        <div className="kb-vatsim-atis-rwy-bearing">
          <span className="kb-vatsim-atis-rwy-bearing-label">{bearingLabel}</span>
          <span className="kb-vatsim-atis-rwy-bearing-value">
            {String(Math.round(vm.bearing)).padStart(3, "0")}°
          </span>
        </div>
      </div>;
  };
  const renderBroadcast = (b, idx) => <div className="kb-vatsim-atis-broadcast" key={`b-${idx}`}>
      <div className="kb-vatsim-atis-broadcast-head">
        <div className="kb-vatsim-atis-info-letter" aria-label={`${infoLabel} ${b.atisCode || "—"}`}>
          {b.atisCode || "—"}
        </div>
        <div className="kb-vatsim-atis-broadcast-meta">
          <div className="kb-vatsim-atis-broadcast-callsign">
            {b.callsign}
            {b.split && <span className="kb-vatsim-atis-split-tag">
                {b.split === "arrival" ? arrivalsBroadcastLabel : departuresBroadcastLabel}
              </span>}
          </div>
          <div className="kb-vatsim-atis-broadcast-freq">
            <span className="kb-vatsim-atis-broadcast-freq-label">{frequencyLabel}</span>
            <span className="kb-vatsim-atis-broadcast-freq-value">{formatFreq(b.frequency)}</span>
          </div>
        </div>
      </div>

      {b.text && <div className="kb-vatsim-atis-transcript-wrap">
          <span className="kb-vatsim-atis-transcript-label">{transcriptLabel}</span>
          <pre className="kb-vatsim-atis-transcript">{b.text}</pre>
        </div>}
    </div>;
  return <div className="kb-vatsim-atis">
      <div className="kb-vatsim-atis-header">
        <span className="kb-vatsim-atis-eyebrow">
          <span className={`kb-vatsim-atis-dot${isOnline ? " kb-vatsim-atis-dot--on" : ""}`} aria-hidden="true" />
          {title}
        </span>
        <span className="kb-vatsim-atis-station">
          <span className="kb-vatsim-atis-station-icao">{code}</span>
          {name && <span className="kb-vatsim-atis-station-name">{name}</span>}
        </span>
      </div>

      {loading && <div className="kb-vatsim-atis-loading">
          <div className="kb-vatsim-atis-skeleton kb-vatsim-atis-skeleton--long" />
          <div className="kb-vatsim-atis-skeleton kb-vatsim-atis-skeleton--short" />
          <div className="kb-vatsim-atis-meta-status">{loadingLabel}</div>
        </div>}

      {!loading && error && <div className="kb-vatsim-atis-message">{errorLabel}</div>}

      {!loading && !error && !isOnline && <div className="kb-vatsim-atis-message">{offlineLabel}</div>}

      {!loading && !error && isOnline && <div className="kb-vatsim-atis-broadcasts">
          {data.broadcasts.map(renderBroadcast)}
        </div>}

      {!loading && !error && isOnline && (wind || altimeter || primary?.approachType) && <div className="kb-vatsim-atis-decoded">
          {wind && <div className="kb-vatsim-atis-stat">
              <span className="kb-vatsim-atis-stat-label">{windLabel}</span>
              <span className="kb-vatsim-atis-stat-value">
                {typeof wind.dir === "number" && <svg className="kb-vatsim-atis-wind-needle" width="11" height="11" viewBox="0 0 12 12" aria-hidden="true" style={{
    transform: `rotate(${wind.dir}deg)`
  }}>
                    <path d="M6 1.2 L9 7.2 L6 5.7 L3 7.2 Z" fill="currentColor" />
                    <line x1="6" y1="5.7" x2="6" y2="10.8" stroke="currentColor" strokeWidth="1.3" strokeLinecap="round" />
                  </svg>}
                {formatWind(wind)}
              </span>
            </div>}
          {altimeter && <div className="kb-vatsim-atis-stat">
              <span className="kb-vatsim-atis-stat-label">{altimeterLabel}</span>
              <span className="kb-vatsim-atis-stat-value">{formatAltimeter(altimeter)}</span>
            </div>}
          {primary?.approachType && <div className="kb-vatsim-atis-stat">
              <span className="kb-vatsim-atis-stat-label">{approachLabel}</span>
              <span className="kb-vatsim-atis-stat-value">{primary.approachType}</span>
            </div>}
        </div>}

      {endsVm.length > 0 && <div className="kb-vatsim-atis-runways-section">
          <div className="kb-vatsim-atis-runways-head">
            <span className="kb-vatsim-atis-runways-eyebrow">{runwaysInUseLabel}</span>
            {magneticVariation && <span className="kb-vatsim-atis-magvar">MAG VAR {magneticVariation}</span>}
          </div>
          <div className="kb-vatsim-atis-runways">{endsVm.map(renderRunwayRow)}</div>
        </div>}

      <div className="kb-vatsim-atis-meta">
        {mostRecentUpdate && <span className="kb-vatsim-atis-meta-time">
            {observedLabel} {ageLabel(mostRecentUpdate)}
          </span>}
        <button type="button" className={"kb-vatsim-atis-refresh" + (refreshing ? " refreshing" : "")} onClick={() => fetchAtis(true)} aria-label={refreshLabel} title={refreshLabel}>
          <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
            <path d="M3 12a9 9 0 0 1 9-9 9.75 9.75 0 0 1 6.74 2.74L21 8" />
            <path d="M21 3v5h-5" />
            <path d="M21 12a9 9 0 0 1-9 9 9.75 9.75 0 0 1-6.74-2.74L3 16" />
            <path d="M3 21v-5h5" />
          </svg>
        </button>
      </div>
    </div>;
};

export const LiveMetar = ({icao, name, decoderUrl, loadingLabel = "Fetching latest observation", errorLabel = "Unable to fetch live METAR. Try the visual decoder.", refreshLabel = "Refresh", decoderLabel = "Metar & Taf Visual decoder", observedLabel = "Observed", agoLabel = "min ago", justNowLabel = "just now", windLabel = "Wind", visibilityLabel = "Visibility", weatherLabel = "Weather", cloudsLabel = "Clouds", tempDewLabel = "Temp · Dew", altimeterLabel = "Altimeter", calmLabel = "Calm", variableLabel = "Variable", gustingLabel = "gusting", fromLabel = "from", atLabel = "at"}) => {
  const code = (icao || "").toUpperCase();
  const [raw, setRaw] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);
  const [refreshing, setRefreshing] = useState(false);
  const [nowTick, setNowTick] = useState(Date.now());
  const WX_CODES = {
    MI: "shallow",
    PR: "partial",
    BC: "patches",
    DR: "drifting",
    BL: "blowing",
    SH: "showers",
    TS: "thunderstorm",
    FZ: "freezing",
    DZ: "drizzle",
    RA: "rain",
    SN: "snow",
    SG: "snow grains",
    IC: "ice crystals",
    PL: "ice pellets",
    GR: "hail",
    GS: "small hail",
    UP: "unknown precip",
    BR: "mist",
    FG: "fog",
    FU: "smoke",
    VA: "volcanic ash",
    DU: "dust",
    SA: "sand",
    HZ: "haze",
    PY: "spray",
    PO: "dust whirls",
    SQ: "squalls",
    FC: "funnel cloud",
    SS: "sandstorm",
    DS: "duststorm"
  };
  const CLOUD_LABELS = {
    FEW: "Few",
    SCT: "Scattered",
    BKN: "Broken",
    OVC: "Overcast",
    CLR: "Clear",
    SKC: "Sky clear",
    NCD: "No clouds",
    NSC: "No significant clouds",
    VV: "Vertical visibility"
  };
  const parseMetar = input => {
    if (!input || typeof input !== "string") return null;
    const beforeRmk = input.split(/\s+RMK\b/)[0].trim();
    if (beforeRmk.length < 10) return null;
    const tokens = beforeRmk.split(/\s+/);
    let i = 0;
    if (tokens[i] === "METAR" || tokens[i] === "SPECI") i++;
    let station = null;
    if ((/^[A-Z]{4}$/).test(tokens[i] || "")) station = tokens[i++];
    let time = null;
    const tm = (tokens[i] || "").match(/^(\d{2})(\d{2})(\d{2})Z$/);
    if (tm) {
      time = {
        day: +tm[1],
        hour: +tm[2],
        minute: +tm[3]
      };
      i++;
    }
    while (tokens[i] === "AUTO" || tokens[i] === "COR" || tokens[i] === "NIL") i++;
    let wind = null;
    const wt = tokens[i] || "";
    if ((/^00000(KT|MPS|KMH)$/).test(wt)) {
      wind = {
        calm: true
      };
      i++;
    } else {
      const wm = wt.match(/^(VRB|\d{3})(\d{2,3})(?:G(\d{2,3}))?(KT|MPS|KMH)$/);
      if (wm) {
        wind = {
          dir: wm[1] === "VRB" ? "VRB" : +wm[1],
          speed: +wm[2],
          gust: wm[3] ? +wm[3] : null,
          unit: wm[4]
        };
        i++;
        if ((/^\d{3}V\d{3}$/).test(tokens[i] || "")) i++;
      }
    }
    let visibility = null;
    if (tokens[i] === "CAVOK") {
      visibility = {
        cavok: true
      };
      i++;
    } else if ((/^\d{4}$/).test(tokens[i] || "")) {
      visibility = {
        value: +tokens[i],
        unit: "m"
      };
      i++;
    } else if ((/^\d+$/).test(tokens[i] || "") && (/^\d+\/\d+SM$/).test(tokens[i + 1] || "")) {
      const whole = +tokens[i];
      const fm = tokens[i + 1].match(/^(\d+)\/(\d+)SM$/);
      visibility = {
        value: whole + +fm[1] / +fm[2],
        unit: "SM"
      };
      i += 2;
    } else {
      const sm = (tokens[i] || "").match(/^(M)?(\d+(?:\/\d+)?)SM$/);
      if (sm) {
        const v = sm[2].includes("/") ? +sm[2].split("/")[0] / +sm[2].split("/")[1] : +sm[2];
        visibility = {
          value: v,
          unit: "SM",
          lessThan: !!sm[1]
        };
        i++;
      }
    }
    const weather = [];
    const wxRe = /^(VC|\+|-)?((?:MI|PR|BC|DR|BL|SH|TS|FZ){0,2})((?:DZ|RA|SN|SG|IC|PL|GR|GS|UP|BR|FG|FU|VA|DU|SA|HZ|PY|PO|SQ|FC|SS|DS)+)$/;
    while (i < tokens.length && wxRe.test(tokens[i])) {
      weather.push(tokens[i]);
      i++;
    }
    const clouds = [];
    while (i < tokens.length) {
      const t = tokens[i];
      if (t === "CLR" || t === "SKC" || t === "NCD" || t === "NSC") {
        clouds.push({
          cover: t,
          base: null
        });
        i++;
      } else {
        const cm = t.match(/^(FEW|SCT|BKN|OVC)(\d{3})(CB|TCU)?$/);
        if (cm) {
          clouds.push({
            cover: cm[1],
            base: +cm[2] * 100,
            type: cm[3] || null
          });
          i++;
          continue;
        }
        const vvm = t.match(/^VV(\d{3}|\/\/\/)$/);
        if (vvm) {
          clouds.push({
            cover: "VV",
            base: vvm[1] === "///" ? null : +vvm[1] * 100
          });
          i++;
          continue;
        }
        break;
      }
    }
    let temp = null;
    let dewpoint = null;
    const td = (tokens[i] || "").match(/^(M)?(\d{2})\/(M)?(\d{2})$/);
    if (td) {
      temp = (td[1] ? -1 : 1) * +td[2];
      dewpoint = (td[3] ? -1 : 1) * +td[4];
      i++;
    }
    let altimeter = null;
    const altA = (tokens[i] || "").match(/^A(\d{4})$/);
    const altQ = (tokens[i] || "").match(/^Q(\d{4})$/);
    if (altA) {
      altimeter = {
        value: +altA[1] / 100,
        unit: "inHg"
      };
      i++;
    } else if (altQ) {
      altimeter = {
        value: +altQ[1],
        unit: "hPa"
      };
      i++;
    }
    const ceilingLayer = clouds.find(c => c.cover === "BKN" || c.cover === "OVC");
    const ceiling = ceilingLayer ? ceilingLayer.base : null;
    let visSM = Infinity;
    if (visibility?.cavok) visSM = 10; else if (visibility?.unit === "SM") visSM = visibility.value; else if (visibility?.unit === "m") visSM = visibility.value / 1609;
    let flightCategory = "VFR";
    if (ceiling !== null && ceiling < 500 || visSM < 1) flightCategory = "LIFR"; else if (ceiling !== null && ceiling < 1000 || visSM < 3) flightCategory = "IFR"; else if (ceiling !== null && ceiling < 3000 || visSM <= 5) flightCategory = "MVFR";
    return {
      station,
      time,
      wind,
      visibility,
      weather,
      clouds,
      temp,
      dewpoint,
      altimeter,
      flightCategory,
      raw: input
    };
  };
  const formatWind = w => {
    if (w.calm) return calmLabel;
    const dir = w.dir === "VRB" ? variableLabel : `${String(w.dir).padStart(3, "0")}°`;
    let s = `${fromLabel} ${dir} ${atLabel} ${w.speed} kt`;
    if (w.gust) s += `, ${gustingLabel} ${w.gust} kt`;
    return s;
  };
  const formatVisibility = v => {
    if (v.cavok) return "CAVOK";
    if (v.unit === "SM") {
      const prefix = v.lessThan ? "<" : "";
      const val = Number.isInteger(v.value) ? v.value : v.value.toFixed(1);
      return `${prefix}${val} SM`;
    }
    if (v.unit === "m") {
      if (v.value >= 9999) return "≥10 km";
      if (v.value >= 1000) return `${(v.value / 1000).toFixed(1)} km`;
      return `${v.value} m`;
    }
    return "";
  };
  const formatWeather = wxList => {
    return wxList.map(tok => {
      let intensity = "";
      let rest = tok;
      if (rest.startsWith("+")) {
        intensity = "Heavy ";
        rest = rest.slice(1);
      } else if (rest.startsWith("-")) {
        intensity = "Light ";
        rest = rest.slice(1);
      } else if (rest.startsWith("VC")) {
        intensity = "Vicinity ";
        rest = rest.slice(2);
      }
      const parts = [];
      for (let p = 0; p < rest.length; p += 2) {
        const w = WX_CODES[rest.substr(p, 2)];
        if (w) parts.push(w);
      }
      const phrase = (intensity + parts.join(" ")).trim();
      return phrase.charAt(0).toUpperCase() + phrase.slice(1);
    }).join(", ");
  };
  const formatClouds = cs => {
    return cs.map(c => {
      const lbl = CLOUD_LABELS[c.cover] || c.cover;
      if (c.base == null) return lbl;
      const ft = c.base.toLocaleString("en-US");
      const type = c.type === "CB" ? " (CB)" : c.type === "TCU" ? " (TCU)" : "";
      return `${lbl} ${ft} ft${type}`;
    }).join(", ");
  };
  const formatTempDew = (t, d) => {
    if (t == null) return null;
    const toF = c => Math.round(c * 9 / 5 + 32);
    let s = `${t}°C / ${toF(t)}°F`;
    if (d != null) s += ` · ${d}°C / ${toF(d)}°F`;
    return s;
  };
  const formatAltimeter = a => {
    if (a.unit === "inHg") {
      return `${a.value.toFixed(2)} inHg · ${Math.round(a.value * 33.8639)} hPa`;
    }
    return `${a.value} hPa · ${(a.value / 33.8639).toFixed(2)} inHg`;
  };
  const fetchMetar = async isUserRefresh => {
    try {
      if (isUserRefresh) setRefreshing(true); else setLoading(true);
      setError(null);
      const res = await fetch(`https://metar.vatsim.net/${code}`, {
        cache: "no-store"
      });
      if (!res.ok) throw new Error("http");
      const text = (await res.text()).trim();
      if (!text || !(/[A-Z]{4}\s+\d{6}Z/).test(text)) {
        setRaw(null);
        setError("nodata");
      } else {
        setRaw(text);
      }
    } catch (e) {
      setError("fetch");
    } finally {
      setLoading(false);
      if (isUserRefresh) {
        setTimeout(() => setRefreshing(false), 600);
      }
    }
  };
  useEffect(() => {
    if (!code) return;
    fetchMetar(false);
    const interval = setInterval(() => fetchMetar(false), 5 * 60 * 1000);
    return () => clearInterval(interval);
  }, [code]);
  useEffect(() => {
    const tick = setInterval(() => setNowTick(Date.now()), 30000);
    return () => clearInterval(tick);
  }, []);
  const metar = raw ? parseMetar(raw) : null;
  const decoder = decoderUrl || `https://metar-taf.com/${code}`;
  const windDeg = typeof metar?.wind?.dir === "number" ? metar.wind.dir : null;
  let obsLabel = null;
  if (metar?.time) {
    const now = new Date();
    let year = now.getUTCFullYear();
    let month = now.getUTCMonth();
    if (metar.time.day > now.getUTCDate()) {
      month -= 1;
      if (month < 0) {
        month = 11;
        year -= 1;
      }
    }
    const obs = new Date(Date.UTC(year, month, metar.time.day, metar.time.hour, metar.time.minute));
    if (!isNaN(obs)) {
      const hh = String(obs.getUTCHours()).padStart(2, "0");
      const mm = String(obs.getUTCMinutes()).padStart(2, "0");
      const diffMin = Math.max(0, Math.round((nowTick - obs.getTime()) / 60000));
      obsLabel = `${observedLabel} ${hh}${mm}Z · ${diffMin === 0 ? justNowLabel : `${diffMin} ${agoLabel}`}`;
    }
  }
  return <div className="kb-metar">
      <div className="kb-metar-header">
        <span className="kb-metar-eyebrow">Weather</span>
        <a className="kb-metar-decoder-link" href={decoder} target="_blank" rel="noopener noreferrer">
          {decoderLabel}
          <svg className="kb-metar-decoder-arrow" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
            <path d="M7 7h10v10" />
            <path d="M7 17 17 7" />
          </svg>
        </a>
      </div>

      <div className="kb-metar-body">
        <div className="kb-metar-icon" aria-hidden="true">
          <svg viewBox="0 0 24 24" width="22" height="22" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
            <path d="M17.7 7.7a2.5 2.5 0 1 1 1.8 4.3H2" />
            <path d="M9.6 4.6A2 2 0 1 1 11 8H2" />
            <path d="M12.6 19.4A2 2 0 1 0 14 16H2" />
          </svg>
        </div>

        <div className="kb-metar-content">
          <div className="kb-metar-title">
            <span className="kb-metar-icao">{code}</span>
            {name ? <span className="kb-metar-name">{name}</span> : null}
            {metar?.flightCategory && <span className={`kb-metar-cat kb-metar-cat--${metar.flightCategory.toLowerCase()}`}>
                {metar.flightCategory}
              </span>}
          </div>

          {loading && <>
              <div className="kb-metar-skeleton kb-metar-skeleton--long" />
              <div className="kb-metar-skeleton kb-metar-skeleton--short" />
              <div className="kb-metar-meta">
                <span className="kb-metar-meta-status">{loadingLabel}</span>
              </div>
            </>}

          {!loading && error && <div className="kb-metar-error">{errorLabel}</div>}

          {!loading && !error && metar && <>
              <pre className="kb-metar-raw">{metar.raw}</pre>
              <div className="kb-metar-decoded">
                {metar.wind && <div className="kb-metar-stat">
                    <span className="kb-metar-stat-label">{windLabel}</span>
                    <span className="kb-metar-stat-value">
                      {windDeg !== null && <svg className="kb-metar-wind-arrow" width="12" height="12" viewBox="0 0 24 24" fill="currentColor" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true" style={{
    transform: `rotate(${windDeg}deg)`
  }}>
                          <polygon points="12 2 19 21 12 17 5 21 12 2" />
                        </svg>}
                      {formatWind(metar.wind)}
                    </span>
                  </div>}
                {metar.visibility && <div className="kb-metar-stat">
                    <span className="kb-metar-stat-label">{visibilityLabel}</span>
                    <span className="kb-metar-stat-value">{formatVisibility(metar.visibility)}</span>
                  </div>}
                {metar.weather.length > 0 && <div className="kb-metar-stat">
                    <span className="kb-metar-stat-label">{weatherLabel}</span>
                    <span className="kb-metar-stat-value">{formatWeather(metar.weather)}</span>
                  </div>}
                {metar.clouds.length > 0 && <div className="kb-metar-stat">
                    <span className="kb-metar-stat-label">{cloudsLabel}</span>
                    <span className="kb-metar-stat-value">{formatClouds(metar.clouds)}</span>
                  </div>}
                {metar.temp != null && <div className="kb-metar-stat">
                    <span className="kb-metar-stat-label">{tempDewLabel}</span>
                    <span className="kb-metar-stat-value">{formatTempDew(metar.temp, metar.dewpoint)}</span>
                  </div>}
                {metar.altimeter && <div className="kb-metar-stat">
                    <span className="kb-metar-stat-label">{altimeterLabel}</span>
                    <span className="kb-metar-stat-value">{formatAltimeter(metar.altimeter)}</span>
                  </div>}
              </div>
              <div className="kb-metar-meta">
                {obsLabel && <span className="kb-metar-meta-time">{obsLabel}</span>}
                <button type="button" className={"kb-metar-refresh" + (refreshing ? " refreshing" : "")} onClick={() => fetchMetar(true)} aria-label={refreshLabel} title={refreshLabel}>
                  <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
                    <path d="M3 12a9 9 0 0 1 9-9 9.75 9.75 0 0 1 6.74 2.74L21 8" />
                    <path d="M21 3v5h-5" />
                    <path d="M21 12a9 9 0 0 1-9 9 9.75 9.75 0 0 1-6.74-2.74L3 16" />
                    <path d="M3 21v-5h5" />
                  </svg>
                </button>
              </div>
            </>}
        </div>
      </div>
    </div>;
};

<Info>For flight-simulation use only - Do **NOT** use for real-world flight</Info>

Toronto Pearson is Air Canada's primary global hub and the largest and busiest airport in Canada. The field operates from two main terminals (T1 for Air Canada Mainline, Rouge, and most Star Alliance partners; T3 for everyone else) and supports independent simultaneous operations on both the east/west pair of runways and the north/south pair. Five runways, more than 40 km of taxiways, the world's largest central de-icing facility, and frequent winter low-visibility operations make Pearson the most operationally complex field in Canada.

## ATIS

<VatsimAtis
  icao="CYYZ"
  name="Toronto Pearson"
  magneticVariation="10°W"
  runways={[
{ designators: ["05", "23"] },
{ designators: ["06L", "24R"] },
{ designators: ["06R", "24L"] },
{ designators: ["15L", "33R"] },
{ designators: ["15R", "33L"] }
]}
/>

## Live Weather

<LiveMetar icao="CYYZ" name="Toronto Pearson" />

## Online ATC Network Maps

<NetworkLinks icao="CYYZ" />

## Runways

<Runways
  magneticVariation="10°W"
  runways={[
{
  designators: ["05", "23"],
  length_ft: 11120,
  length_m: 3389,
  width_ft: 200,
  surface: "Asphalt",
  ils: { "05": "CAT IIIA", "23": "CAT I" },
  lighting: "HIRL, CL",
  notes: "One of the two longest runways at Pearson. RWY 05 is CAT III capable for heavy winter operations."
},
{
  designators: ["06L", "24R"],
  length_ft: 9697,
  length_m: 2956,
  width_ft: 200,
  surface: "Asphalt",
  ils: { "06L": "CAT IIIA", "24R": "CAT I" },
  lighting: "HIRL, CL",
  notes: "Primary parallel runway. CAT IIIA on 06L supports operations down to RVR 600 ft."
},
{
  designators: ["06R", "24L"],
  length_ft: 9000,
  length_m: 2743,
  width_ft: 200,
  surface: "Asphalt",
  ils: { "06R": "CAT I", "24L": "CAT I" },
  lighting: "HIRL, CL",
  notes: "Outer parallel runway. Commonly used for arrivals during east operations."
},
{
  designators: ["15L", "33R"],
  length_ft: 11050,
  length_m: 3368,
  width_ft: 200,
  surface: "Asphalt",
  ils: { "15L": "CAT I", "33R": "CAT I" },
  lighting: "HIRL, CL",
  notes: "North/south runway. Used during strong crosswinds on the east/west runways and for noise routing south."
},
{
  designators: ["15R", "33L"],
  length_ft: 9088,
  length_m: 2770,
  width_ft: 200,
  surface: "Asphalt",
  ils: { "15R": "CAT I", "33L": "CAT I" },
  lighting: "HIRL, CL",
  notes: "North/south parallel runway, closest to Terminal 1."
}
]}
/>

## ATC Frequencies

<AtcFrequencies
  frequencies={[
{ role: "ATIS", freqs: ["120.825"] },
{ role: "Clearance Delivery", freqs: ["121.30"] },
{ role: "Ground", freqs: [
  { value: "119.10" },
  { value: "121.65" },
  { value: "121.90", note: "by area" }
]},
{ role: "Tower", freqs: [
  { value: "118.35" },
  { value: "118.70", note: "by runway pair" }
]},
{ role: "Departure", subLabel: "Toronto Terminal", freqs: ["127.575", "128.80"] },
{ role: "Arrival", subLabel: "Toronto Terminal", freqs: ["124.475", "125.40", "132.80", "134.175"] }
]}
/>

Confirm the latest frequency assignments against the current Canada Air Pilot (CAP) and the live <Tooltip tip="Automatic Terminal Information Service" cta="See glossary" href="/aops/additional-resources/glossary/atis">ATIS</Tooltip>. Multiple Ground and Tower frequencies are split geographically; always read back the position you were instructed to contact, not the one you guessed.

## Gates

<Gates
  terminal="Terminal 1"
  sections={[
{
  type: "domestic",
  label: "Domestic",
  range: "Concourse D",
  gates: ["D1, D3, D5", "D7-D12", "D20, D22, D24, D26, D28", "D31-D45", "D51, D53, D55, D57"],
  operators: ["Air Canada Mainline", "Air Canada Express"],
  aircraft: ["A220", "A319/A320/A321", "E175", "Q400"],
  notes: "Primary narrowbody domestic stands. Numbering is not contiguous (D20-D28 odd-skip and D51-D57 odd-only). Air Canada Express turboprop and regional jet operations off the lower D gates (D1-D12)."
},
{
  type: "international",
  label: "International",
  range: "Concourse E (E68-E81)",
  gates: ["E68-E81"],
  operators: ["Air Canada Mainline"],
  aircraft: ["B777-300ER", "B787-8/-9", "A330-300", "A321XLR"],
  notes: "Widebody pier. E73 and E75 are dual-jet-bridge stands certified for the A380 (used by Emirates and Etihad). All E gates accept widebody pushback with tow."
},
{
  type: "transborder",
  label: "US Transborder",
  range: "Concourse F (F60-F99)",
  gates: ["F60-F63", "F64A/B", "F65", "F66A/B", "F82-F99"],
  operators: ["Air Canada Mainline", "Air Canada Express"],
  aircraft: ["A220", "A319/A320/A321", "E175", "B787 (HNL/deep US)"],
  notes: "US Customs Border Protection preclearance is performed at the F-concourse departure level before boarding. F64 and F66 are split into A/B stands. Expect tighter pushback windows on US-bound flights."
},
{
  type: "remote",
  label: "Remote / Cargo",
  range: "Apron VI / VII",
  operators: ["Air Canada Cargo"],
  aircraft: ["B767F", "B777F (interline)"],
  notes: "Hardstand positions on the south apron used for overnight parking, cargo ops, and gate-shortage holds during heavy banks. Bus transfer for passenger ops on the rare occasion they are used."
}
]}
/>

Air Canada does not operate from Terminal 3. Star Alliance interline traffic stays inside Terminal 1; LH, NH, UA, AC, OS, LX, and SQ all gate on Concourse E for international widebody arrivals and departures.

## Airport at a Glance

| Item               | Value                          |
| ------------------ | ------------------------------ |
| ICAO / IATA        | CYYZ / YYZ                     |
| City               | Toronto / Mississauga, Ontario |
| Field elevation    | 569 ft (173 m)                 |
| Magnetic variation | 10°W                           |
| Time zone          | America/Toronto (EST / EDT)    |
| Air Canada role    | Primary global hub             |

## Charts

<ChartsLinks icao="CYYZ" />

## Common Procedures

* **Runway selection**: Pearson operates either east or west depending on wind. East operations use 05, 06L, and 06R for departures and arrivals; west operations use 23, 24R, and 24L. The north/south runways (15/33) are used in strong cross-component conditions or for noise routing toward Lake Ontario at night.
* **Parallel operations**: Independent simultaneous arrivals or departures are normal on the east/west pair (06L/24R and 06R/24L) and on the north/south pair (15L/33R and 15R/33L). Triple operations are possible in good weather using one runway from each pair plus the diagonal.
* **De-icing**: The Central De-icing Facility (CDF) is one of the largest in the world, with six bays handling up to twelve aircraft at once. Expect a hold short of the assigned de-ice bay during winter ops, and follow Iceman ground crew on the dedicated CDF freq.
* **Noise routing**: After 0030L and before 0630L, jet operations are restricted to specific runway ends and headings to reduce overflight of residential areas to the north and west.

## Hot Spots and Local Hazards

* **Taxi network complexity**: With more than 40 km of taxiways, the route from a remote stand to runway 06L or 15L can include up to a dozen named segments. Brief every hold-short and every Hot Spot from the airport diagram before brakes release. Apply the [Plan, Brief, Verify framework](/aops/standard-procedures/taxi-operations) on every taxi.
* **Hot Spots 1 and 2** (east side): the intersections near the Cargo apron and on the south side of the inner east/west parallel are charted as high-incursion-risk locations.
* **Winter low-visibility**: Pearson averages 110 to 130 cm of snow per season. Expect SMGCS procedures, illuminated stop bars, and reduced taxi speed (one-third of normal) whenever RVR falls below 1,200 ft.
* **De-icing holdover times**: After Type IV is applied, monitor the holdover-time clock. Taxi delays on the way to the runway can erode holdover quickly.
* **Convergent traffic on parallels**: During east operations, departing traffic from 06L and 06R climbs on diverging headings within the first 1.5 nm. Maintain visual separation and follow the published departure procedure exactly.

<Card title="Next: Montréal-Trudeau (CYUL)" icon="plane-departure" href="/aops/airports/montreal-trudeau">
  Continue to the Montréal-Trudeau briefing
</Card>
