{"id":85151,"date":"2026-05-05T16:13:25","date_gmt":"2026-05-05T23:13:25","guid":{"rendered":"https:\/\/www.redfin.com\/news\/?page_id=85151"},"modified":"2026-08-06T12:58:00","modified_gmt":"2026-08-06T19:58:00","slug":"downloads","status":"publish","type":"page","link":"https:\/\/www.redfin.com\/news\/data-center\/downloads\/","title":{"rendered":"Downloads"},"content":{"rendered":"\t\t<div data-elementor-type=\"wp-page\" data-elementor-id=\"85151\" class=\"elementor elementor-85151\" data-elementor-post-type=\"page\">\n\t\t\t\t<div class=\"elementor-element elementor-element-5c87bfd e-flex e-con-boxed e-con e-parent\" data-id=\"5c87bfd\" data-element_type=\"container\" data-e-type=\"container\">\n\t\t\t\t\t<div class=\"e-con-inner\">\n\t\t\t\t<div class=\"elementor-element elementor-element-2455eee elementor-widget elementor-widget-template\" data-id=\"2455eee\" data-element_type=\"widget\" data-e-type=\"widget\" data-widget_type=\"template.default\">\n\t\t\t\t<div class=\"elementor-widget-container\">\n\t\t\t\t\t\t\t<div class=\"elementor-template\">\n\t\t\t\t\t<div data-elementor-type=\"container\" data-elementor-id=\"85190\" class=\"elementor elementor-85190\" data-elementor-post-type=\"elementor_library\">\n\t\t\t\t<div class=\"elementor-element elementor-element-98128b4 e-flex e-con-boxed e-con e-parent\" data-id=\"98128b4\" data-element_type=\"container\" data-e-type=\"container\">\n\t\t\t\t\t<div class=\"e-con-inner\">\n\t\t<div class=\"elementor-element elementor-element-2e49d5d2 e-con-full e-flex e-con e-child\" data-id=\"2e49d5d2\" data-element_type=\"container\" data-e-type=\"container\">\n\t\t\t\t<div class=\"elementor-element elementor-element-96aef1b e-fit_to_content e-n-menu-none dc-mega-menu e-n-menu-layout-horizontal elementor-widget elementor-widget-n-menu\" data-id=\"96aef1b\" data-element_type=\"widget\" data-e-type=\"widget\" data-settings=\"{&quot;menu_items&quot;:[{&quot;item_title&quot;:&quot;Data Center&quot;,&quot;item_dropdown_content&quot;:&quot;&quot;,&quot;item_icon_active&quot;:null,&quot;item_link&quot;:{&quot;url&quot;:&quot;https:\\\/\\\/redfin.com\\\/news\\\/data-center\\\/&quot;,&quot;is_external&quot;:&quot;&quot;,&quot;nofollow&quot;:&quot;&quot;,&quot;custom_attributes&quot;:&quot;&quot;},&quot;_id&quot;:&quot;1972fcf&quot;,&quot;item_icon&quot;:{&quot;value&quot;:&quot;&quot;,&quot;library&quot;:&quot;&quot;},&quot;element_id&quot;:&quot;&quot;},{&quot;item_title&quot;:&quot;Dashboards&quot;,&quot;item_dropdown_content&quot;:&quot;yes&quot;,&quot;item_icon_active&quot;:null,&quot;_id&quot;:&quot;48cf2c5&quot;,&quot;item_link&quot;:{&quot;url&quot;:&quot;&quot;,&quot;is_external&quot;:&quot;&quot;,&quot;nofollow&quot;:&quot;&quot;,&quot;custom_attributes&quot;:&quot;&quot;},&quot;item_icon&quot;:{&quot;value&quot;:&quot;&quot;,&quot;library&quot;:&quot;&quot;},&quot;element_id&quot;:&quot;&quot;},{&quot;item_title&quot;:&quot;Downloads&quot;,&quot;item_dropdown_content&quot;:&quot;&quot;,&quot;item_icon_active&quot;:null,&quot;item_link&quot;:{&quot;url&quot;:&quot;https:\\\/\\\/redfin.com\\\/news\\\/data-center\\\/downloads\\\/&quot;,&quot;is_external&quot;:&quot;&quot;,&quot;nofollow&quot;:&quot;&quot;,&quot;custom_attributes&quot;:&quot;&quot;},&quot;_id&quot;:&quot;2dbf668&quot;,&quot;item_icon&quot;:{&quot;value&quot;:&quot;&quot;,&quot;library&quot;:&quot;&quot;},&quot;element_id&quot;:&quot;&quot;},{&quot;item_title&quot;:&quot;Methodology&quot;,&quot;item_dropdown_content&quot;:&quot;&quot;,&quot;item_icon_active&quot;:null,&quot;item_link&quot;:{&quot;url&quot;:&quot;https:\\\/\\\/redfin.com\\\/news\\\/data-center\\\/methodology\\\/&quot;,&quot;is_external&quot;:&quot;&quot;,&quot;nofollow&quot;:&quot;&quot;,&quot;custom_attributes&quot;:&quot;&quot;},&quot;_id&quot;:&quot;9d4e024&quot;,&quot;item_icon&quot;:{&quot;value&quot;:&quot;&quot;,&quot;library&quot;:&quot;&quot;},&quot;element_id&quot;:&quot;&quot;}],&quot;content_width&quot;:&quot;fit_to_content&quot;,&quot;content_horizontal_position&quot;:&quot;left&quot;,&quot;item_position_horizontal&quot;:&quot;center&quot;,&quot;breakpoint_selector&quot;:&quot;none&quot;,&quot;item_layout&quot;:&quot;horizontal&quot;,&quot;open_on&quot;:&quot;hover&quot;,&quot;horizontal_scroll&quot;:&quot;disable&quot;,&quot;menu_item_title_distance_from_content&quot;:{&quot;unit&quot;:&quot;px&quot;,&quot;size&quot;:0,&quot;sizes&quot;:[]},&quot;menu_item_title_distance_from_content_tablet&quot;:{&quot;unit&quot;:&quot;px&quot;,&quot;size&quot;:&quot;&quot;,&quot;sizes&quot;:[]},&quot;menu_item_title_distance_from_content_mobile&quot;:{&quot;unit&quot;:&quot;px&quot;,&quot;size&quot;:&quot;&quot;,&quot;sizes&quot;:[]}}\" data-widget_type=\"mega-menu.default\">\n\t\t\t\t\t\t\t<nav class=\"e-n-menu\" data-widget-number=\"158\" aria-label=\"Menu\">\n\t\t\t\t\t<button class=\"e-n-menu-toggle\" id=\"menu-toggle-158\" aria-haspopup=\"true\" aria-expanded=\"false\" aria-controls=\"menubar-158\" aria-label=\"Menu Toggle\">\n\t\t\t<span class=\"e-n-menu-toggle-icon e-open\">\n\t\t\t\t<i class=\"eicon-menu-bar\"><\/i>\t\t\t<\/span>\n\t\t\t<span class=\"e-n-menu-toggle-icon e-close\">\n\t\t\t\t<i class=\"eicon-close\"><\/i>\t\t\t<\/span>\n\t\t<\/button>\n\t\t\t\t\t<div class=\"e-n-menu-wrapper\" id=\"menubar-158\" aria-labelledby=\"menu-toggle-158\">\n\t\t\t\t<ul class=\"e-n-menu-heading\">\n\t\t\t\t\t\t\t\t<li class=\"e-n-menu-item\">\n\t\t\t\t<div id=\"e-n-menu-title-1581\" class=\"e-n-menu-title\">\n\t\t\t\t\t<a class=\"e-n-menu-title-container e-focus e-link\" href=\"https:\/\/redfin.com\/news\/data-center\/\">\t\t\t\t\t\t\t\t\t\t\t\t<span class=\"e-n-menu-title-text\">\n\t\t\t\t\t\t\tData Center\t\t\t\t\t\t<\/span>\n\t\t\t\t\t<\/a>\t\t\t\t\t\t\t\t\t<\/div>\n\t\t\t\t\t\t\t<\/li>\n\t\t\t\t\t<li class=\"e-n-menu-item\">\n\t\t\t\t<div id=\"e-n-menu-title-1582\" class=\"e-n-menu-title\">\n\t\t\t\t\t<div class=\"e-n-menu-title-container\">\t\t\t\t\t\t\t\t\t\t\t\t<span class=\"e-n-menu-title-text\">\n\t\t\t\t\t\t\tDashboards\t\t\t\t\t\t<\/span>\n\t\t\t\t\t<\/div>\t\t\t\t\t\t\t\t\t\t\t<button id=\"e-n-menu-dropdown-icon-1582\" class=\"e-n-menu-dropdown-icon e-focus\" data-tab-index=\"2\" aria-haspopup=\"true\" aria-expanded=\"false\" aria-controls=\"e-n-menu-content-1582\" >\n\t\t\t\t\t\t\t<span class=\"e-n-menu-dropdown-icon-opened\">\n\t\t\t\t\t\t\t\t<i aria-hidden=\"true\" class=\"fas fa-angle-up\"><\/i>\t\t\t\t\t\t\t\t<span class=\"elementor-screen-only\">Close Dashboards<\/span>\n\t\t\t\t\t\t\t<\/span>\n\t\t\t\t\t\t\t<span class=\"e-n-menu-dropdown-icon-closed\">\n\t\t\t\t\t\t\t\t<i aria-hidden=\"true\" class=\"fas fa-angle-down\"><\/i>\t\t\t\t\t\t\t\t<span class=\"elementor-screen-only\">Open Dashboards<\/span>\n\t\t\t\t\t\t\t<\/span>\n\t\t\t\t\t\t<\/button>\n\t\t\t\t\t\t\t\t\t<\/div>\n\t\t\t\t\t\t\t\t\t<div class=\"e-n-menu-content\">\n\t\t\t\t\t\t<div id=\"e-n-menu-content-1582\" data-tab-index=\"2\" aria-labelledby=\"e-n-menu-dropdown-icon-1582\" class=\"elementor-element elementor-element-50633696 e-con-full e-flex e-con e-child\" data-id=\"50633696\" data-element_type=\"container\" data-e-type=\"container\">\n\t\t<div class=\"elementor-element elementor-element-b69f745 e-con-full e-flex e-con e-child\" data-id=\"b69f745\" data-element_type=\"container\" data-e-type=\"container\">\n\t\t\t\t<div class=\"elementor-element elementor-element-39eb209b elementor-nav-menu--dropdown-none elementor-widget elementor-widget-nav-menu\" data-id=\"39eb209b\" data-element_type=\"widget\" data-e-type=\"widget\" data-settings=\"{&quot;layout&quot;:&quot;vertical&quot;,&quot;submenu_icon&quot;:{&quot;value&quot;:&quot;&lt;i class=\\&quot;fas fa-caret-down\\&quot; aria-hidden=\\&quot;true\\&quot;&gt;&lt;\\\/i&gt;&quot;,&quot;library&quot;:&quot;fa-solid&quot;}}\" data-widget_type=\"nav-menu.default\">\n\t\t\t\t\t\t\t\t<nav aria-label=\"Menu\" class=\"elementor-nav-menu--main elementor-nav-menu__container elementor-nav-menu--layout-vertical e--pointer-none\">\n\t\t\t\t<ul id=\"menu-1-39eb209b\" class=\"elementor-nav-menu sm-vertical\"><li class=\"menu-item menu-item-type-post_type menu-item-object-page menu-item-85181\"><a href=\"https:\/\/www.redfin.com\/news\/data-center\/housing-market\/\" class=\"elementor-item menu-link\">Housing Market Tracker: Weekly &#038; Monthly<\/a><\/li>\n<li class=\"menu-item menu-item-type-post_type menu-item-object-page menu-item-85184\"><a href=\"https:\/\/www.redfin.com\/news\/data-center\/buyers-sellers\/\" class=\"elementor-item menu-link\">Balance of Power: Buyers and Sellers<\/a><\/li>\n<li class=\"menu-item menu-item-type-post_type menu-item-object-page menu-item-85180\"><a href=\"https:\/\/www.redfin.com\/news\/data-center\/luxury-home-market\/\" class=\"elementor-item menu-link\">Luxury Home Market<\/a><\/li>\n<li class=\"menu-item menu-item-type-post_type menu-item-object-page menu-item-85179\"><a href=\"https:\/\/www.redfin.com\/news\/data-center\/price-drops\/\" class=\"elementor-item menu-link\">Price Drops<\/a><\/li>\n<li class=\"menu-item menu-item-type-post_type menu-item-object-page menu-item-85182\"><a href=\"https:\/\/www.redfin.com\/news\/data-center\/home-purchase-cancellations\/\" class=\"elementor-item menu-link\">Home Purchase Cancellations<\/a><\/li>\n<li class=\"menu-item menu-item-type-post_type menu-item-object-page menu-item-85183\"><a href=\"https:\/\/www.redfin.com\/news\/data-center\/home-delistings-relistings\/\" class=\"elementor-item menu-link\">Home Delistings &#038; Relistings<\/a><\/li>\n<li class=\"menu-item menu-item-type-post_type menu-item-object-page menu-item-86654\"><a href=\"https:\/\/www.redfin.com\/news\/data-center\/investor-home-purchases\/\" class=\"elementor-item menu-link\">Investor Home Purchases<\/a><\/li>\n<\/ul>\t\t\t<\/nav>\n\t\t\t\t\t\t<nav class=\"elementor-nav-menu--dropdown elementor-nav-menu__container\" aria-hidden=\"true\">\n\t\t\t\t<ul id=\"menu-2-39eb209b\" class=\"elementor-nav-menu sm-vertical\"><li class=\"menu-item menu-item-type-post_type menu-item-object-page menu-item-85181\"><a href=\"https:\/\/www.redfin.com\/news\/data-center\/housing-market\/\" class=\"elementor-item menu-link\" tabindex=\"-1\">Housing Market Tracker: Weekly &#038; Monthly<\/a><\/li>\n<li class=\"menu-item menu-item-type-post_type menu-item-object-page menu-item-85184\"><a href=\"https:\/\/www.redfin.com\/news\/data-center\/buyers-sellers\/\" class=\"elementor-item menu-link\" tabindex=\"-1\">Balance of Power: Buyers and Sellers<\/a><\/li>\n<li class=\"menu-item menu-item-type-post_type menu-item-object-page menu-item-85180\"><a href=\"https:\/\/www.redfin.com\/news\/data-center\/luxury-home-market\/\" class=\"elementor-item menu-link\" tabindex=\"-1\">Luxury Home Market<\/a><\/li>\n<li class=\"menu-item menu-item-type-post_type menu-item-object-page menu-item-85179\"><a href=\"https:\/\/www.redfin.com\/news\/data-center\/price-drops\/\" class=\"elementor-item menu-link\" tabindex=\"-1\">Price Drops<\/a><\/li>\n<li class=\"menu-item menu-item-type-post_type menu-item-object-page menu-item-85182\"><a href=\"https:\/\/www.redfin.com\/news\/data-center\/home-purchase-cancellations\/\" class=\"elementor-item menu-link\" tabindex=\"-1\">Home Purchase Cancellations<\/a><\/li>\n<li class=\"menu-item menu-item-type-post_type menu-item-object-page menu-item-85183\"><a href=\"https:\/\/www.redfin.com\/news\/data-center\/home-delistings-relistings\/\" class=\"elementor-item menu-link\" tabindex=\"-1\">Home Delistings &#038; Relistings<\/a><\/li>\n<li class=\"menu-item menu-item-type-post_type menu-item-object-page menu-item-86654\"><a href=\"https:\/\/www.redfin.com\/news\/data-center\/investor-home-purchases\/\" class=\"elementor-item menu-link\" tabindex=\"-1\">Investor Home Purchases<\/a><\/li>\n<\/ul>\t\t\t<\/nav>\n\t\t\t\t\t\t<\/div>\n\t\t\t\t<\/div>\n\t\t<div class=\"elementor-element elementor-element-39e33ad1 e-con-full e-flex e-con e-child\" data-id=\"39e33ad1\" data-element_type=\"container\" data-e-type=\"container\">\n\t\t\t\t<div class=\"elementor-element elementor-element-4776451d elementor-nav-menu--dropdown-none elementor-widget elementor-widget-nav-menu\" data-id=\"4776451d\" data-element_type=\"widget\" data-e-type=\"widget\" data-settings=\"{&quot;layout&quot;:&quot;vertical&quot;,&quot;submenu_icon&quot;:{&quot;value&quot;:&quot;&lt;i class=\\&quot;fas fa-caret-down\\&quot; aria-hidden=\\&quot;true\\&quot;&gt;&lt;\\\/i&gt;&quot;,&quot;library&quot;:&quot;fa-solid&quot;}}\" data-widget_type=\"nav-menu.default\">\n\t\t\t\t\t\t\t\t<nav aria-label=\"Menu\" class=\"elementor-nav-menu--main elementor-nav-menu__container elementor-nav-menu--layout-vertical e--pointer-none\">\n\t\t\t\t<ul id=\"menu-1-4776451d\" class=\"elementor-nav-menu sm-vertical\"><li class=\"menu-item menu-item-type-post_type menu-item-object-page menu-item-85185\"><a href=\"https:\/\/www.redfin.com\/news\/data-center\/starter-home-market\/\" class=\"elementor-item menu-link\">Starter Home Market<\/a><\/li>\n<li class=\"menu-item menu-item-type-post_type menu-item-object-page menu-item-85186\"><a href=\"https:\/\/www.redfin.com\/news\/data-center\/home-price-index\/\" class=\"elementor-item menu-link\">Redfin Home Price Index<\/a><\/li>\n<li class=\"menu-item menu-item-type-post_type menu-item-object-page menu-item-85189\"><a href=\"https:\/\/www.redfin.com\/news\/data-center\/existing-home-sales\/\" class=\"elementor-item menu-link\">Existing Home Sales<\/a><\/li>\n<li class=\"menu-item menu-item-type-post_type menu-item-object-page menu-item-85188\"><a href=\"https:\/\/www.redfin.com\/news\/data-center\/financing-trends\/\" class=\"elementor-item menu-link\">Financing Trends: Cash Purchases, Loan Types &#038; Down Payments<\/a><\/li>\n<li class=\"menu-item menu-item-type-post_type menu-item-object-page menu-item-86300\"><a href=\"https:\/\/www.redfin.com\/news\/data-center\/migration-patterns\/\" class=\"elementor-item menu-link\">U.S. Migration Patterns<\/a><\/li>\n<li class=\"menu-item menu-item-type-post_type menu-item-object-page menu-item-86647\"><a href=\"https:\/\/www.redfin.com\/news\/data-center\/housing-market-property-types\/\" class=\"elementor-item menu-link\">Housing Market Tracker: Property Types<\/a><\/li>\n<li class=\"menu-item menu-item-type-post_type menu-item-object-page menu-item-86770\"><a href=\"https:\/\/www.redfin.com\/news\/data-center\/housing-affordability-tracker\/\" class=\"elementor-item menu-link\">Housing Affordability Tracker<\/a><\/li>\n<\/ul>\t\t\t<\/nav>\n\t\t\t\t\t\t<nav class=\"elementor-nav-menu--dropdown elementor-nav-menu__container\" aria-hidden=\"true\">\n\t\t\t\t<ul id=\"menu-2-4776451d\" class=\"elementor-nav-menu sm-vertical\"><li class=\"menu-item menu-item-type-post_type menu-item-object-page menu-item-85185\"><a href=\"https:\/\/www.redfin.com\/news\/data-center\/starter-home-market\/\" class=\"elementor-item menu-link\" tabindex=\"-1\">Starter Home Market<\/a><\/li>\n<li class=\"menu-item menu-item-type-post_type menu-item-object-page menu-item-85186\"><a href=\"https:\/\/www.redfin.com\/news\/data-center\/home-price-index\/\" class=\"elementor-item menu-link\" tabindex=\"-1\">Redfin Home Price Index<\/a><\/li>\n<li class=\"menu-item menu-item-type-post_type menu-item-object-page menu-item-85189\"><a href=\"https:\/\/www.redfin.com\/news\/data-center\/existing-home-sales\/\" class=\"elementor-item menu-link\" tabindex=\"-1\">Existing Home Sales<\/a><\/li>\n<li class=\"menu-item menu-item-type-post_type menu-item-object-page menu-item-85188\"><a href=\"https:\/\/www.redfin.com\/news\/data-center\/financing-trends\/\" class=\"elementor-item menu-link\" tabindex=\"-1\">Financing Trends: Cash Purchases, Loan Types &#038; Down Payments<\/a><\/li>\n<li class=\"menu-item menu-item-type-post_type menu-item-object-page menu-item-86300\"><a href=\"https:\/\/www.redfin.com\/news\/data-center\/migration-patterns\/\" class=\"elementor-item menu-link\" tabindex=\"-1\">U.S. Migration Patterns<\/a><\/li>\n<li class=\"menu-item menu-item-type-post_type menu-item-object-page menu-item-86647\"><a href=\"https:\/\/www.redfin.com\/news\/data-center\/housing-market-property-types\/\" class=\"elementor-item menu-link\" tabindex=\"-1\">Housing Market Tracker: Property Types<\/a><\/li>\n<li class=\"menu-item menu-item-type-post_type menu-item-object-page menu-item-86770\"><a href=\"https:\/\/www.redfin.com\/news\/data-center\/housing-affordability-tracker\/\" class=\"elementor-item menu-link\" tabindex=\"-1\">Housing Affordability Tracker<\/a><\/li>\n<\/ul>\t\t\t<\/nav>\n\t\t\t\t\t\t<\/div>\n\t\t\t\t<\/div>\n\t\t\t\t<\/div>\n\t\t\t\t\t\t\t<\/div>\n\t\t\t\t\t\t\t<\/li>\n\t\t\t\t\t<li class=\"e-n-menu-item\">\n\t\t\t\t<div id=\"e-n-menu-title-1583\" class=\"e-n-menu-title\">\n\t\t\t\t\t<a class=\"e-n-menu-title-container e-focus e-link\" href=\"https:\/\/redfin.com\/news\/data-center\/downloads\/\">\t\t\t\t\t\t\t\t\t\t\t\t<span class=\"e-n-menu-title-text\">\n\t\t\t\t\t\t\tDownloads\t\t\t\t\t\t<\/span>\n\t\t\t\t\t<\/a>\t\t\t\t\t\t\t\t\t<\/div>\n\t\t\t\t\t\t\t<\/li>\n\t\t\t\t\t<li class=\"e-n-menu-item\">\n\t\t\t\t<div id=\"e-n-menu-title-1584\" class=\"e-n-menu-title\">\n\t\t\t\t\t<a class=\"e-n-menu-title-container e-focus e-link\" href=\"https:\/\/redfin.com\/news\/data-center\/methodology\/\">\t\t\t\t\t\t\t\t\t\t\t\t<span class=\"e-n-menu-title-text\">\n\t\t\t\t\t\t\tMethodology\t\t\t\t\t\t<\/span>\n\t\t\t\t\t<\/a>\t\t\t\t\t\t\t\t\t<\/div>\n\t\t\t\t\t\t\t<\/li>\n\t\t\t\t\t\t<\/ul>\n\t\t\t<\/div>\n\t\t<\/nav>\n\t\t\t\t\t\t<\/div>\n\t\t\t\t<\/div>\n\t\t\t\t\t<\/div>\n\t\t\t\t<\/div>\n\t\t<div class=\"elementor-element elementor-element-3d7ea12d e-flex e-con-boxed e-con e-parent\" data-id=\"3d7ea12d\" data-element_type=\"container\" data-e-type=\"container\">\n\t\t\t\t\t<div class=\"e-con-inner\">\n\t\t<div class=\"elementor-element elementor-element-3875cd54 e-con-full e-flex e-con e-child\" data-id=\"3875cd54\" data-element_type=\"container\" data-e-type=\"container\">\n\t\t\t\t<div class=\"elementor-element elementor-element-3b769059 elementor-widget elementor-widget-breadcrumbs\" data-id=\"3b769059\" data-element_type=\"widget\" data-e-type=\"widget\" data-widget_type=\"breadcrumbs.default\">\n\t\t\t\t\t<p id=\"breadcrumbs\"><span><span><a href=\"https:\/\/www.redfin.com\/news\/\">Home<\/a><\/span><\/span><\/p>\t\t\t\t<\/div>\n\t\t\t\t<\/div>\n\t\t\t\t\t<\/div>\n\t\t\t\t<\/div>\n\t\t\t\t<\/div>\n\t\t\t\t<\/div>\n\t\t\t\t\t\t<\/div>\n\t\t\t\t<\/div>\n\t\t\t\t\t<\/div>\n\t\t\t\t<\/div>\n\t\t<div class=\"elementor-element elementor-element-6d89299 e-flex e-con-boxed e-con e-parent\" data-id=\"6d89299\" data-element_type=\"container\" data-e-type=\"container\">\n\t\t\t\t\t<div class=\"e-con-inner\">\n\t\t<div class=\"elementor-element elementor-element-eadd71b e-con-full e-flex e-con e-child\" data-id=\"eadd71b\" data-element_type=\"container\" data-e-type=\"container\">\n\t\t\t\t<div class=\"elementor-element elementor-element-39ddf32 elementor-widget elementor-widget-heading\" data-id=\"39ddf32\" data-element_type=\"widget\" data-e-type=\"widget\" data-widget_type=\"heading.default\">\n\t\t\t\t\t<h1 class=\"elementor-heading-title elementor-size-default\"><span style=\"color: var(--e-global-color-primary)\">Redfin<\/span> Data Center Download Hub<\/h1>\t\t\t\t<\/div>\n\t\t\t\t<div class=\"elementor-element elementor-element-b884e9d elementor-widget elementor-widget-heading\" data-id=\"b884e9d\" data-element_type=\"widget\" data-e-type=\"widget\" data-widget_type=\"heading.default\">\n\t\t\t\t\t<h2 class=\"elementor-heading-title elementor-size-default\">Download housing market insights from across the U.S.<\/h2>\t\t\t\t<\/div>\n\t\t\t\t<\/div>\n\t\t\t\t\t<\/div>\n\t\t\t\t<\/div>\n\t\t<div class=\"elementor-element elementor-element-8f0e182 e-flex e-con-boxed e-con e-parent\" data-id=\"8f0e182\" data-element_type=\"container\" data-e-type=\"container\">\n\t\t\t\t\t<div class=\"e-con-inner\">\n\t\t\t\t<div class=\"elementor-element elementor-element-92f3a83 elementor-widget elementor-widget-html\" data-id=\"92f3a83\" data-element_type=\"widget\" data-e-type=\"widget\" data-widget_type=\"html.default\">\n\t\t\t\t\t<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n<meta charset=\"UTF-8\">\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n<title>Redfin Data Center &mdash; Downloads<\/title>\n<style>\n  .dc-downloads {\n    --red: #D92228;\n    --red-hover: #b51c21;\n    --bg: #f7f7f9;\n    --card: #ffffff;\n    --dark: #222222;\n    --gray: #5e6d77;\n    --light-gray: #767676;\n    --border: #d1d5d8;\n    --disabled: #dcdcdc;\n    font-family: -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, Helvetica, Arial, sans-serif;\n    color: #222222;\n    line-height: 1.5;\n    margin: 0 auto;\n  }\n  .dc-downloads * { box-sizing: border-box; }\n\n  \/* Grid \u2014 stretch so cards in the same row share height *\/\n  .dc-downloads .grid {\n    display: grid;\n    grid-template-columns: repeat(3, 1fr);\n    gap: 20px;\n    align-items: stretch;\n  }\n  @media (max-width: 900px) { .dc-downloads .grid { grid-template-columns: repeat(2, 1fr); } }\n  @media (max-width: 580px) { .dc-downloads .grid { grid-template-columns: 1fr; } }\n\n  \/* Card *\/\n  .dc-downloads .card {\n    background: var(--card);\n    border-radius: 8px;\n    box-shadow: 0 4px 16px rgba(0,0,0,0.12);\n    padding: 20px;\n    display: flex;\n    flex-direction: column;\n  }\n  .dc-downloads .card-title { font-size: 17px; font-weight: 700; margin-bottom: 3px; }\n  .dc-downloads .card-badge {\n    font-size: 10px; font-weight: 700; text-transform: uppercase;\n    letter-spacing: 0.5px; color: var(--light-gray); margin-bottom: 4px;\n  }\n  .dc-downloads .card-last-updated {\n    font-size: 11px; color: var(--light-gray); margin-bottom: 8px; display: none;\n  }\n  .dc-downloads .card-desc { font-size: 13px; color: var(--gray); margin-bottom: 14px; }\n\n  \/* Frequency toggle *\/\n  .dc-downloads .freq-toggle { display: flex; gap: 4px; margin-bottom: 14px; }\n  .dc-downloads .freq-btn {\n    padding: 5px 14px; border-radius: 6px; font-size: 12px; font-weight: 600;\n    cursor: pointer; border: 1px solid #b8bdc4;\n    background: white; color: var(--dark); transition: all 0.15s;\n    box-shadow: 0 1px 2px rgba(0,0,0,0.08);\n  }\n  .dc-downloads .freq-btn:hover:not(.active) {\n    background: #ecedef; border-color: var(--dark);\n    box-shadow: 0 2px 4px rgba(0,0,0,0.12);\n  }\n  .dc-downloads .freq-btn.active { background: var(--dark); color: white; border-color: var(--dark); box-shadow: none; }\n\n  \/* Column mode + suffix toggles *\/\n  .dc-downloads .col-mode-row { display: flex; align-items: center; justify-content: space-between; margin-bottom: 6px; }\n  .dc-downloads .col-mode-label { font-size: 11px; color: var(--gray); }\n  .dc-downloads .col-mode-toggle, .dc-downloads .suffix-toggle { display: flex; gap: 4px; }\n  .dc-downloads .col-mode-btn, .dc-downloads .suffix-btn {\n    padding: 4px 12px; border-radius: 6px; font-size: 11px; font-weight: 600;\n    cursor: pointer; border: 1px solid #b8bdc4;\n    background: white; color: var(--dark); transition: all 0.15s;\n    box-shadow: 0 1px 2px rgba(0,0,0,0.08);\n  }\n  .dc-downloads .col-mode-btn:hover:not(.active), .dc-downloads .suffix-btn:hover:not(.active):not(:disabled) {\n    background: #ecedef; border-color: var(--dark);\n    box-shadow: 0 2px 4px rgba(0,0,0,0.12);\n  }\n  .dc-downloads .col-mode-btn.active, .dc-downloads .suffix-btn.active {\n    background: var(--dark); color: white; border-color: var(--dark);\n    box-shadow: none;\n  }\n  .dc-downloads .suffix-btn:disabled { opacity: 0.35; cursor: not-allowed; box-shadow: none; }\n\n  \/* Key metrics info panel *\/\n  \/* Metrics info block (sits near the title; only the toggle is interactive) *\/\n  .dc-downloads .metrics-info {\n    background: #f8f8f8; border: 1px solid var(--border); border-radius: 4px;\n    padding: 12px; margin-bottom: 24px;\n  }\n  .dc-downloads .metrics-info-header {\n    display: flex; align-items: center; justify-content: space-between; margin-bottom: 10px;\n  }\n  .dc-downloads .metrics-info-label {\n    font-size: 11px; font-weight: 600; color: var(--gray);\n    text-transform: uppercase; letter-spacing: 0.3px;\n  }\n  .dc-downloads .metrics-pills {\n    display: none; flex-wrap: wrap; gap: 4px 6px;\n  }\n  .dc-downloads .metrics-pills.visible { display: flex; }\n  .dc-downloads .metric-pill {\n    display: inline-block; padding: 3px 10px; border-radius: 12px;\n    background: #ecedef; color: #4a5560;\n    font-size: 11px; font-weight: 500; line-height: 1.4;\n    cursor: default; user-select: none;\n  }\n  \/* Extra breathing room before the download button *\/\n  .dc-downloads .trends-row { margin-bottom: 20px; }\n\n  \/* Form *\/\n  .dc-downloads .form-group { margin-bottom: 12px; }\n  .dc-downloads .form-label { display: block; font-size: 12px; font-weight: 600; margin-bottom: 5px; }\n\n  .dc-downloads .select-wrapper { position: relative; }\n  .dc-downloads .select-wrapper::after {\n    content: '\\25be'; font-size: 11px; color: var(--gray);\n    position: absolute; right: 9px; top: 50%; transform: translateY(-50%); pointer-events: none;\n  }\n  .dc-downloads select, .dc-downloads input[type=\"text\"] {\n    width: 100%; padding: 9px 28px 9px 11px; border: 1px solid var(--border);\n    border-radius: 4px; font-size: 13px; color: var(--dark);\n    appearance: none; outline: none; background: #fff;\n  }\n  .dc-downloads select:focus, .dc-downloads input[type=\"text\"]:focus { border-color: var(--red); }\n  .dc-downloads select:disabled { background: #f3f3f3; color: var(--light-gray); cursor: not-allowed; }\n\n  \/* Date range *\/\n  .dc-downloads .date-range-row { display: flex; gap: 8px; }\n  .dc-downloads .date-col { flex: 1; }\n  .dc-downloads .date-sublabel {\n    font-size: 10px; font-weight: 700; text-transform: uppercase;\n    letter-spacing: 0.5px; color: var(--light-gray); margin-bottom: 4px;\n  }\n  .dc-downloads .date-selects { display: flex; gap: 5px; }\n  .dc-downloads .date-selects .select-wrapper { flex: 1; }\n  .dc-downloads .date-selects select { padding: 9px 22px 9px 9px; font-size: 12px; }\n\n  \/* Button \u2014 pushed to bottom of card *\/\n  .dc-downloads .btn-download {\n    width: 100%; padding: 11px; margin-top: auto; margin-bottom: 6px;\n    background: var(--disabled); color: #fff; border: none; border-radius: 4px;\n    font-size: 14px; font-weight: 600; cursor: not-allowed;\n    transition: background 0.2s;\n  }\n  .dc-downloads .btn-download.active { background: var(--red); cursor: pointer; }\n  .dc-downloads .btn-download.active:hover { background: var(--red-hover); }\n  .dc-downloads .btn-download.loading { background: #a0a0a0; cursor: wait; }\n\n  .dc-downloads .tiny-text { font-size: 11px; color: var(--light-gray); text-align: center; }\n  .dc-downloads .error-msg { font-size: 12px; color: var(--red); margin-top: 5px; display: none; }\n\n  \/* Coming soon notice *\/\n  .dc-downloads .coming-soon-notice {\n    font-size: 12px; color: #856404; background: #fff8e1;\n    border: 1px solid #ffe082; border-radius: 4px;\n    padding: 8px 10px; margin-bottom: 14px;\n  }\n\n  \/* Tips modal \u2014 dialog is document-level, kept unscoped *\/\n  #tips-dialog {\n    border: none;\n    border-radius: 8px;\n    padding: 0;\n    width: 420px;\n    max-width: 90vw;\n    box-shadow: 0 8px 32px rgba(0,0,0,0.18);\n    font-family: -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, Helvetica, Arial, sans-serif;\n    position: fixed;\n    top: 50%;\n    left: 50%;\n    transform: translate(-50%, -50%);\n    margin: 0;\n  }\n  #tips-dialog::backdrop {\n    background: rgba(0,0,0,0.45);\n  }\n  .tips-header {\n    display: flex;\n    justify-content: space-between;\n    align-items: center;\n    padding: 16px 20px 12px;\n    border-bottom: 1px solid #d1d5d8;\n  }\n  .tips-header h3 {\n    margin: 0;\n    font-size: 15px;\n    font-weight: 700;\n    color: #222222;\n  }\n  .tips-close {\n    background: none;\n    border: none;\n    font-size: 18px;\n    color: #5e6d77;\n    cursor: pointer;\n    padding: 0;\n    line-height: 1;\n  }\n  .tips-body {\n    padding: 16px 20px;\n  }\n  .tips-body ul {\n    margin: 0;\n    padding: 0;\n    list-style: none;\n  }\n  .tips-body li {\n    font-size: 13px;\n    color: #222222;\n    padding: 7px 0;\n    border-bottom: 1px solid #f0f0f0;\n    display: flex;\n    gap: 10px;\n    align-items: flex-start;\n    line-height: 1.5;\n  }\n  .tips-body li:last-child { border-bottom: none; }\n  .tips-icon { font-size: 15px; flex-shrink: 0; margin-top: 1px; }\n  .tips-footer {\n    display: flex;\n    justify-content: space-between;\n    align-items: center;\n    padding: 12px 20px 16px;\n    border-top: 1px solid #d1d5d8;\n  }\n  .tips-dont-show {\n    display: flex;\n    align-items: center;\n    gap: 6px;\n    font-size: 12px;\n    color: #5e6d77;\n    cursor: pointer;\n  }\n  .tips-dont-show input { cursor: pointer; }\n  .tips-download-btn {\n    background: #D92228;\n    color: #fff;\n    border: none;\n    border-radius: 4px;\n    padding: 9px 18px;\n    font-size: 13px;\n    font-weight: 600;\n    cursor: pointer;\n    transition: background 0.2s;\n  }\n  .tips-download-btn:hover { background: #b51c21; }\n<\/style>\n<\/head>\n<body>\n<div class=\"page dc-downloads\">\n  <div class=\"grid\" id=\"grid\">\n    <div class=\"card\" id=\"card-housing_market\"><div class=\"card-title\">Housing Market Tracker<\/div><\/div>\n    <div class=\"card\" id=\"card-property_types\"><div class=\"card-title\">Housing Market Tracker by Property Type<\/div><\/div>\n    <div class=\"card\" id=\"card-buyers_and_sellers\"><div class=\"card-title\">Balance of Power: Buyers and Sellers<\/div><\/div>\n    <div class=\"card\" id=\"card-luxury\"><div class=\"card-title\">Luxury Home Market<\/div><\/div>\n    <div class=\"card\" id=\"card-price_drops\"><div class=\"card-title\">Price Drops<\/div><\/div>\n    <div class=\"card\" id=\"card-contract_cancellations\"><div class=\"card-title\">Home Purchase Cancellations<\/div><\/div>\n    <div class=\"card\" id=\"card-delistings_relistings\"><div class=\"card-title\">Home Delistings &amp; Relistings<\/div><\/div>\n    <div class=\"card\" id=\"card-investors\"><div class=\"card-title\">Investor Home Purchases<\/div><\/div>\n    <div class=\"card\" id=\"card-starter_home\"><div class=\"card-title\">Starter Home Market<\/div><\/div>\n    <div class=\"card\" id=\"card-rhpi\"><div class=\"card-title\">Redfin Home Price Index<\/div><\/div>\n    <div class=\"card\" id=\"card-ehs\"><div class=\"card-title\">Existing Home Sales<\/div><\/div>\n    <div class=\"card\" id=\"card-cash_loan\"><div class=\"card-title\">Financing Trends: Cash Purchases, Loan Types &amp; Down Payments<\/div><\/div>\n    <div class=\"card\" id=\"card-migration\"><div class=\"card-title\">U.S. Migration Patterns<\/div><\/div>\n    <div class=\"card\" id=\"card-affordability\"><div class=\"card-title\">Housing Affordability Tracker<\/div><\/div>\n  <\/div>\n<\/div>\n\n<dialog id=\"tips-dialog\">\n  <div class=\"tips-header\">\n    <h3>Tips for using this data<\/h3>\n    <button class=\"tips-close\" id=\"tips-close-btn\" aria-label=\"Close\">&times;<\/button>\n  <\/div>\n  <div class=\"tips-body\">\n    <ul>\n      <li>\n        <span class=\"tips-icon\">&#128269;<\/span>\n        <span><strong>Find a region:<\/strong> Press <strong>Cmd+F<\/strong> (Mac) or <strong>Ctrl+F<\/strong> (PC) and search by name in the <em>Region Name<\/em> column.<\/span>\n      <\/li>\n      <li>\n        <span class=\"tips-icon\">&#128196;<\/span>\n        <span><strong>Data structure:<\/strong> Each row represents one region for one time period, defined by the <em>Period Begin<\/em> and <em>Period End<\/em> columns.<\/span>\n      <\/li>\n      <li>\n        <span class=\"tips-icon\">&#128288;<\/span>\n        <span><strong>Column suffixes:<\/strong> <em>YOY<\/em> = year-over-year change, <em>WOW<\/em> = week-over-week change, <em>MOM<\/em> = month-over-month change.<\/span>\n      <\/li>\n      <li>\n        <span class=\"tips-icon\">&#128200;<\/span>\n        <span><strong>Rank regions:<\/strong> Sort any metric column to compare regions &mdash; e.g. sort <em>Median Sale Price<\/em> descending to find the most expensive markets.<\/span>\n      <\/li>\n      <li>\n        <span class=\"tips-icon\">&#8211;<\/span>\n        <span><strong>Missing values:<\/strong> <em>NA<\/em> means no data was available for that region or time period.<\/span>\n      <\/li>\n    <\/ul>\n  <\/div>\n  <div class=\"tips-footer\">\n    <label class=\"tips-dont-show\">\n      <input type=\"checkbox\" id=\"tips-dont-show-cb\">\n      Don't show again\n    <\/label>\n    <button class=\"tips-download-btn\" id=\"tips-download-btn\">Download Data<\/button>\n  <\/div>\n<\/dialog>\n\n<script>\nconst S3 = 'https:\/\/redfin-public-data.s3.us-west-2.amazonaws.com\/redfin_data_center';\n\n\/\/ ---------------------------------------------------------------------------\n\/\/ Card configurations\n\/\/ Derived at init() \u2014 no wait required:\n\/\/   badge     \u2192 freqToggle[0].badge (via FREQ_DISPLAY)\n\/\/   pillsSrc  \u2192 first metro 'all' CSV from geosByFreq\n\/\/   freqToggle label \/ dateType \/ badge \u2192 FREQ_DISPLAY lookup by key\n\/\/ Geo paths in geosByFreq are hardcoded as instant fallback; index.json\n\/\/ overrides them at runtime once the async fetch completes.\n\/\/ ---------------------------------------------------------------------------\nconst CARDS = [\n  {\n    id: 'housing_market',\n    title: 'Housing Market Tracker',\n    desc: 'Comprehensive housing market data for residential homes across the U.S., including sales, pricing, inventory, and more.',\n    dateCol: 'period_begin',\n    freqToggle: [\n      { key: 'monthly',    startYear: 2012 },\n      { key: 'four_weeks', startYear: 2016 },\n    ],\n    keyColumns: [\n      'HOMES SOLD', 'MEDIAN SALE PRICE ($)',\n      'MEDIAN DAYS ON MARKET (DAYS)', 'NEW LISTINGS', 'ACTIVE LISTINGS',\n      'PENDING SALES', 'MEDIAN NEW LISTING PRICE PER SQ.FT. ($)',\n    ],\n    keyColumnsByFreq: {\n      monthly:    ['HOMES SOLD', 'MEDIAN SALE PRICE ($)', 'MEDIAN DAYS ON MARKET (DAYS)', 'NEW LISTINGS', 'ACTIVE LISTINGS', 'INVENTORY', 'PENDING SALES', 'MEDIAN NEW LISTING PRICE PER SQ.FT. ($)'],\n      four_weeks: ['HOMES SOLD', 'MEDIAN SALE PRICE ($)', 'MEDIAN DAYS ON MARKET (DAYS)', 'NEW LISTINGS', 'ACTIVE LISTINGS', 'PENDING SALES', 'MEDIAN NEW LISTING PRICE PER SQ.FT. NSA ($)'],\n    },\n    geosByFreq: {\n      monthly: [\n        { type: 'country',      label: 'United States',  all: 'housing_market\/monthly\/country.csv' },\n        { type: 'state',        label: 'State',          all: 'housing_market\/monthly\/all_states.csv' },\n        { type: 'metro',        label: 'Metro Area',     all: 'housing_market\/monthly\/all_metros.csv',         top50: 'housing_market\/monthly\/top_50_metros.csv' },\n        { type: 'county',       label: 'County',         all: 'housing_market\/monthly\/all_counties.csv',       in_top_50_metros: 'housing_market\/monthly\/counties_in_top_50_metros.csv' },\n        { type: 'city',         label: 'City',           all: 'housing_market\/monthly\/all_cities.csv',         top50: 'housing_market\/monthly\/top_50_cities.csv' },\n        { type: 'zip',          label: 'Zip Code',       all: 'housing_market\/monthly\/all_zips.csv',           in_top_50_metros: 'housing_market\/monthly\/zips_in_top_50_metros.csv' },\n        { type: 'neighborhood', label: 'Neighborhood',   all: 'housing_market\/monthly\/all_neighborhoods.csv',  in_top_50_metros: 'housing_market\/monthly\/nbhds_in_top_50_metros.csv' },\n      ],\n      four_weeks: [\n        { type: 'country', label: 'United States', all: 'housing_market\/weekly\/country.csv' },\n        { type: 'metro',   label: 'Metro Area',    all: 'housing_market\/weekly\/all_metros.csv', top50: 'housing_market\/weekly\/top_50_metros.csv' },\n      ],\n    },\n  },\n  {\n    id: 'property_types',\n    title: 'Housing Market Tracker by Property Type',\n    desc: 'Monthly housing market data broken out by property type \u2014 Single Family Residential, Townhouse, Condo\/Co-op, and Multi-Family (2-4 Units) \u2014 across all U.S. geographies.',\n    dateCol: 'period_begin',\n    noWow: true,\n    propertyTypes: ['Single Family Residential', 'Townhouse', 'Condo\/Co-op', 'Multi-Family (2-4 Units)'],\n    freqToggle: [\n      { key: 'monthly', label: 'Monthly', startYear: 2012, dateType: 'monthly', badge: 'Updated Monthly' },\n    ],\n    keyColumns: [\n      'HOMES SOLD', 'MEDIAN SALE PRICE NSA ($)', 'MEDIAN DAYS ON MARKET (DAYS)',\n      'NEW LISTINGS', 'ACTIVE LISTINGS', 'PENDING SALES',\n    ],\n    geosByFreq: {\n      monthly: [\n        { type: 'country',      label: 'United States',  all: 'property_types\/monthly\/country.csv' },\n        { type: 'state',        label: 'State',          all: 'property_types\/monthly\/all_states.csv' },\n        { type: 'metro',        label: 'Metro Area',     all: 'property_types\/monthly\/all_metros.csv',         top50: 'property_types\/monthly\/top_50_metros.csv' },\n        { type: 'county',       label: 'County',         all: 'property_types\/monthly\/all_counties.csv',       in_top_50_metros: 'property_types\/monthly\/counties_in_top_50_metros.csv' },\n        { type: 'city',         label: 'City',           all: 'property_types\/monthly\/all_cities.csv',         top50: 'property_types\/monthly\/top_50_cities.csv' },\n        { type: 'zip',          label: 'Zip Code',       all: 'property_types\/monthly\/all_zips.csv',           in_top_50_metros: 'property_types\/monthly\/zips_in_top_50_metros.csv' },\n        { type: 'neighborhood', label: 'Neighborhood',   all: 'property_types\/monthly\/all_neighborhoods.csv',  in_top_50_metros: 'property_types\/monthly\/nbhds_in_top_50_metros.csv' },\n      ],\n    },\n  },\n  {\n    id: 'buyers_and_sellers',\n    title: 'Balance of Power: Buyers and Sellers',\n    desc: 'Seasonally adjusted monthly index measuring whether conditions favor buyers or sellers across U.S. metro areas.',\n    dateCol: 'period_begin',\n    noMom: true,\n    noWow: true,\n    noMetricToggle: true,\n    keyColumns: ['BUYERS', 'SELLERS', 'BUYER-SELLER RATIO', 'SELLER-BUYER % DIFFERENCE'],\n    startYear: 2016,\n    dateType: 'monthly',\n    badge: 'Updated Monthly',\n    geos: [\n      { type: 'country',       label: 'United States', all: 'buyers_and_sellers\/monthly\/country.csv' },\n      { type: 'census_region', label: 'Census Region', all: 'buyers_and_sellers\/monthly\/all_census_regions.csv' },\n      { type: 'metro',         label: 'Metro Area',    all: 'buyers_and_sellers\/monthly\/top_50_metros.csv', coverageLabel: 'Top 50 most populous metro areas' },\n    ],\n  },\n  {\n    id: 'luxury',\n    title: 'Luxury Home Market',\n    desc: 'Monthly luxury and non-luxury home market activity across U.S. metros and states, including sales, prices, listings, and days on market.',\n    dateCol: 'period_begin',\n    noMom: true,\n    noWow: true,\n    keyColumns: ['MEDIAN SALE PRICE ($)', 'PENDING SALES', 'ACTIVE LISTINGS', 'MEDIAN DAYS ON MARKET (DAYS)'],\n    excludeColumns: ['price bucket'],\n    freqToggle: [\n      { key: 'luxury',     label: 'Luxury',     startYear: 2013, dateType: 'monthly', badge: 'Updated Monthly' },\n      { key: 'non_luxury', label: 'Non-Luxury', startYear: 2013, dateType: 'monthly', badge: 'Updated Monthly' },\n      { key: 'both',       label: 'Both',       startYear: 2013, dateType: 'monthly', badge: 'Updated Monthly' },\n    ],\n    geosByFreq: {\n      luxury: [\n        { type: 'country', label: 'United States', all: 'luxury\/luxury\/country.csv' },\n        { type: 'state',   label: 'State',         all: 'luxury\/luxury\/state.csv' },\n        { type: 'metro',   label: 'Metro Area',    all: 'luxury\/luxury\/all_metros.csv', coverageLabel: 'Top 50 most populous metro areas' },\n      ],\n      non_luxury: [\n        { type: 'country', label: 'United States', all: 'luxury\/non_luxury\/country.csv' },\n        { type: 'state',   label: 'State',         all: 'luxury\/non_luxury\/state.csv' },\n        { type: 'metro',   label: 'Metro Area',    all: 'luxury\/non_luxury\/all_metros.csv', coverageLabel: 'Top 50 most populous metro areas' },\n      ],\n      both: [\n        { type: 'country', label: 'United States', all: 'luxury\/both\/country.csv' },\n        { type: 'state',   label: 'State',         all: 'luxury\/both\/state.csv' },\n        { type: 'metro',   label: 'Metro Area',    all: 'luxury\/both\/all_metros.csv', coverageLabel: 'Top 50 most populous metro areas' },\n      ],\n    },\n  },\n  {\n    id: 'price_drops',\n    title: 'Price Drops',\n    desc: 'Price drop data for residential homes across the U.S., including count of listings with price drops, drop magnitude, and share of active and sold listings affected.',\n    dateCol: 'period_begin',\n    freqToggle: [\n      { key: 'monthly',    startYear: 2012 },\n      { key: 'four_weeks', startYear: 2016 },\n    ],\n    keyColumns: [\n      'PRICE DROPS', 'PERCENT ACTIVE WITH PRICE DROPS (%)', 'AVERAGE SIZE OF PRICE DROP (%)', 'HOMES SOLD WITH PRICE DROPS',\n    ],\n    noMetricToggle: true,\n    geosByFreq: {\n      monthly: [\n        { type: 'country',      label: 'United States',  all: 'price_drops\/monthly\/country.csv' },\n        { type: 'state',        label: 'State',          all: 'price_drops\/monthly\/all_states.csv' },\n        { type: 'metro',        label: 'Metro Area',     all: 'price_drops\/monthly\/all_metros.csv',         top50: 'price_drops\/monthly\/top_50_metros.csv' },\n        { type: 'county',       label: 'County',         all: 'price_drops\/monthly\/all_counties.csv',       in_top_50_metros: 'price_drops\/monthly\/counties_in_top_50_metros.csv' },\n        { type: 'city',         label: 'City',           all: 'price_drops\/monthly\/all_cities.csv',         top50: 'price_drops\/monthly\/top_50_cities.csv' },\n        { type: 'zip',          label: 'Zip Code',       all: 'price_drops\/monthly\/all_zips.csv',           in_top_50_metros: 'price_drops\/monthly\/zips_in_top_50_metros.csv' },\n        { type: 'neighborhood', label: 'Neighborhood',   all: 'price_drops\/monthly\/all_neighborhoods.csv',  in_top_50_metros: 'price_drops\/monthly\/nbhds_in_top_50_metros.csv' },\n      ],\n      four_weeks: [\n        { type: 'country', label: 'United States', all: 'price_drops\/weekly\/country.csv' },\n        { type: 'metro',   label: 'Metro Area',    all: 'price_drops\/weekly\/all_metros.csv', top50: 'price_drops\/weekly\/top_50_metros.csv' },\n      ],\n    },\n  },\n  {\n    id: 'contract_cancellations',\n    title: 'Home Purchase Cancellations',\n    desc: 'Contract cancellations for residential homes across the U.S., including count and cancellation rate as a share of pending sales.',\n    dateCol: 'period_begin',\n    freqToggle: [\n      { key: 'monthly',    startYear: 2012 },\n      { key: 'four_weeks', startYear: 2016 },\n    ],\n    keyColumns: [\n      'HOME PURCHASE CANCELLATIONS', 'PERCENT OF PENDING SALES (%)',\n    ],\n    noMetricToggle: true,\n    geosByFreq: {\n      monthly: [\n        { type: 'country',      label: 'United States',  all: 'contract_cancellations\/monthly\/country.csv' },\n        { type: 'state',        label: 'State',          all: 'contract_cancellations\/monthly\/all_states.csv' },\n        { type: 'metro',        label: 'Metro Area',     all: 'contract_cancellations\/monthly\/all_metros.csv',         top50: 'contract_cancellations\/monthly\/top_50_metros.csv' },\n        { type: 'county',       label: 'County',         all: 'contract_cancellations\/monthly\/all_counties.csv',       in_top_50_metros: 'contract_cancellations\/monthly\/counties_in_top_50_metros.csv' },\n        { type: 'city',         label: 'City',           all: 'contract_cancellations\/monthly\/all_cities.csv',         top50: 'contract_cancellations\/monthly\/top_50_cities.csv' },\n        { type: 'zip',          label: 'Zip Code',       all: 'contract_cancellations\/monthly\/all_zips.csv',           in_top_50_metros: 'contract_cancellations\/monthly\/zips_in_top_50_metros.csv' },\n        { type: 'neighborhood', label: 'Neighborhood',   all: 'contract_cancellations\/monthly\/all_neighborhoods.csv',  in_top_50_metros: 'contract_cancellations\/monthly\/nbhds_in_top_50_metros.csv' },\n      ],\n      four_weeks: [\n        { type: 'country', label: 'United States', all: 'contract_cancellations\/weekly\/country.csv' },\n        { type: 'metro',   label: 'Metro Area',    all: 'contract_cancellations\/weekly\/all_metros.csv', top50: 'contract_cancellations\/weekly\/top_50_metros.csv' },\n      ],\n    },\n  },\n  {\n    id: 'delistings_relistings',\n    title: 'Home Delistings & Relistings',\n    desc: 'Delisting and relisting activity for residential homes across the U.S., including permanent delistings, relistings, days on market before delisting, and time to relist.',\n    dateCol: 'period_begin',\n    freqToggle: [\n      { key: 'monthly',    startYear: 2012 },\n      { key: 'four_weeks', startYear: 2016 },\n    ],\n    keyColumns: [\n      'TOTAL DELISTINGS', 'SHARE OF LISTINGS DELISTED (%)',\n      'TOTAL RELISTINGS', 'SHARE OF LISTINGS RELISTED (%)',\n    ],\n    noMetricToggle: true,\n    geosByFreq: {\n      monthly: [\n        { type: 'country',      label: 'United States',  all: 'delistings_relistings\/monthly\/country.csv' },\n        { type: 'state',        label: 'State',          all: 'delistings_relistings\/monthly\/all_states.csv' },\n        { type: 'metro',        label: 'Metro Area',     all: 'delistings_relistings\/monthly\/all_metros.csv',         top50: 'delistings_relistings\/monthly\/top_50_metros.csv' },\n        { type: 'county',       label: 'County',         all: 'delistings_relistings\/monthly\/all_counties.csv',       in_top_50_metros: 'delistings_relistings\/monthly\/counties_in_top_50_metros.csv' },\n        { type: 'city',         label: 'City',           all: 'delistings_relistings\/monthly\/all_cities.csv',         top50: 'delistings_relistings\/monthly\/top_50_cities.csv' },\n        { type: 'zip',          label: 'Zip Code',       all: 'delistings_relistings\/monthly\/all_zips.csv',           in_top_50_metros: 'delistings_relistings\/monthly\/zips_in_top_50_metros.csv' },\n        { type: 'neighborhood', label: 'Neighborhood',   all: 'delistings_relistings\/monthly\/all_neighborhoods.csv',  in_top_50_metros: 'delistings_relistings\/monthly\/nbhds_in_top_50_metros.csv' },\n      ],\n      four_weeks: [\n        { type: 'country', label: 'United States', all: 'delistings_relistings\/weekly\/country.csv' },\n        { type: 'metro',   label: 'Metro Area',    all: 'delistings_relistings\/weekly\/all_metros.csv', top50: 'delistings_relistings\/weekly\/top_50_metros.csv' },\n      ],\n    },\n  },\n  {\n    id: 'investors',\n    title: 'Investor Home Purchases',\n    desc: 'Quarterly investor home purchase activity and market share across U.S. metros, with breakdowns by price tier and property type.',\n    dateCol: 'period_begin',\n    noMetricToggle: true,\n    noMom: true,\n    noWow: true,\n    freqToggle: [\n      { key: 'by_metro',    startYear: 2000, geoLabel: 'Select Geography', geoPlaceholder: 'Select geography type...' },\n      { key: 'by_category', startYear: 2000, geoLabel: 'Select Category',  geoPlaceholder: 'Select category type...'  },\n    ],\n    keyColumnsByFreq: {\n      by_metro:    ['INVESTOR HOME PURCHASES', 'INVESTOR MARKET SHARE (%)'],\n      by_category: ['INVESTOR HOME PURCHASES', 'INVESTOR MARKET SHARE (%)', 'SHARE OF INVESTOR HOME PURCHASES (%)'],\n    },\n    geosByFreq: {\n      by_metro: [\n        { type: 'country', label: 'United States', all: 'investors\/by_metro\/country.csv' },\n        { type: 'metro',   label: 'Metro Area',    all: 'investors\/by_metro\/all_metros.csv', coverageLabel: 'Available Metros' },\n      ],\n      by_category: [\n        { type: 'price_tier',    label: 'Price Tier',    all: 'investors\/by_category\/price_tier.csv' },\n        { type: 'property_type', label: 'Property Type', all: 'investors\/by_category\/property_type.csv' },\n      ],\n    },\n  },\n  {\n    id: 'starter_home',\n    title: 'Starter Home Market',\n    desc: 'Entry-level home market activity (5th\\u201335th percentile by sale price) across U.S. metros and states, with sales, prices, listings, and days on market.',\n    dateCol: 'period_begin',\n    noMom: true,\n    noWow: true,\n    keyColumns: ['MEDIAN SALE PRICE ($)', 'PENDING SALES', 'ACTIVE LISTINGS', 'MEDIAN DAYS ON MARKET (DAYS)'],\n    startYear: 2013,\n    dateType: 'monthly',\n    badge: 'Updated Monthly',\n    pillsSrc: 'starter_homes\/all_metros.csv',\n    geos: [\n      { type: 'country', label: 'United States', all: 'starter_homes\/country.csv' },\n      { type: 'state',   label: 'State',         all: 'starter_homes\/state.csv' },\n      { type: 'metro',   label: 'Metro Area',     all: 'starter_homes\/all_metros.csv', coverageLabel: 'Top 50 most populous metro areas' },\n    ],\n  },\n  {\n    id: 'rhpi',\n    title: 'Redfin Home Price Index',\n    dateCol: 'period_begin',\n    startYear: 2012,\n    dateType: 'monthly',\n    badge: 'Updated Monthly',\n    keyColumns: [\n      'REDFIN HOME PRICE INDEX',\n    ],\n    noMetricToggle: true,\n    noWow: true,\n    pillsSrc: 'rhpi\/monthly\/all_metros.csv',\n    geos: [\n      { type: 'country', label: 'United States', all: 'rhpi\/monthly\/country.csv' },\n      { type: 'metro',   label: 'Metro Area',    all: 'rhpi\/monthly\/all_metros.csv', coverageLabel: 'Top 50 most populous metro areas' },\n    ],\n  },\n  {\n    id: 'ehs',\n    title: 'Existing Home Sales',\n    badge: 'Updated Monthly',\n    desc: 'Closed transactions for previously owned homes, NAR-adjusted and seasonally adjusted monthly.',\n    noMom: true,\n    noWow: true,\n    noMetricToggle: true,\n    keyColumns: ['EXISTING HOME SALES', 'SEASONALLY ADJUSTED ANNUAL RATE'],\n    startYear: 2010,\n    dateType: 'monthly',\n    dateCol: 'period_begin',\n    pillsSrc: 'ehs\/monthly\/all_census_regions.csv',\n    geos: [\n      { type: 'country',       label: 'United States', all: 'ehs\/monthly\/country.csv' },\n      { type: 'census_region', label: 'Census Region', all: 'ehs\/monthly\/all_census_regions.csv' },\n    ],\n  },\n  {\n    id: 'cash_loan',\n    title: 'Financing Trends: Cash Purchases, Loan Types & Down Payments',\n    desc: 'Monthly trends in home purchase financing across U.S. metros \\u2014 share of all-cash buyers, down payments, and loan type breakdown (conventional, FHA, VA, jumbo).',\n    dateCol: 'period_begin',\n    noMom: true,\n    noWow: true,\n    noMetricToggle: true,\n    keyColumns: [\n      'PERCENT ALL CASH (%)',\n      'MEDIAN DOWN PAYMENT ($)',\n      'MEDIAN DOWN PAYMENT PCT (%)',\n      'PERCENT FHA LOAN (%)',\n      'PERCENT VA LOAN (%)',\n      'PERCENT CONVENTIONAL LOAN (%)',\n      'PERCENT CONVENTIONAL CONFORMING LOAN (%)',\n      'PERCENT CONVENTIONAL JUMBO LOAN (%)',\n    ],\n    startYear: 2011,\n    dateType: 'monthly',\n    badge: 'Updated Quarterly',\n    geos: [\n      { type: 'country', label: 'United States', all: 'all_cash_loan_types\/country.csv' },\n      { type: 'metro',   label: 'Metro Area',    all: 'all_cash_loan_types\/all_metros.csv', coverageLabel: 'Available Metros' },\n    ],\n  },\n  {\n    id: 'migration',\n    title: 'U.S. Migration Patterns',\n    desc: 'Quarterly migration trends showing where people are searching to move across U.S. metros and states, with origin-destination pairs and regional inflow\/outflow breakdowns.',\n    dateCol: 'period_begin',\n    noMom: true,\n    noWow: true,\n    noMetricToggle: true,\n    badge: 'Updated Quarterly',\n    startYear: 2021,\n    freqToggle: [\n      { key: 'od_pairs',   geoLabel: 'Select Geography', geoPlaceholder: 'Select geography type...' },\n      { key: 'aggregated', geoLabel: 'Select Geography', geoPlaceholder: 'Select geography type...' },\n    ],\n    keyColumnsByFreq: {\n      od_pairs:   ['FLOW', 'REVERSE FLOW', 'NET FLOW (ORIGIN TO DESTINATION)', 'GROSS', 'PCT OF ORIGIN OUTFLOW (%)', 'PCT OF DESTINATION INFLOW (%)'],\n      aggregated: ['INFLOW', 'OUTFLOW', 'NET FLOW', 'GROSS', 'PCT SEARCHING OUT (%)'],\n    },\n    geosByFreq: {\n      od_pairs: [\n        { type: 'csa',   label: 'Metro Area (CSA)', all: 'migration_traffic\/od_pairs\/all_metros.csv', coverageLabel: 'Top 100 most populous metro areas' },\n        { type: 'state', label: 'State',            all: 'migration_traffic\/od_pairs\/all_states.csv' },\n      ],\n      aggregated: [\n        { type: 'national', label: 'United States',   all: 'migration_traffic\/aggregated\/national.csv' },\n        { type: 'csa',      label: 'Metro Area (CSA)', all: 'migration_traffic\/aggregated\/all_metros.csv', coverageLabel: 'Top 100 most populous metro areas' },\n        { type: 'state',    label: 'State',            all: 'migration_traffic\/aggregated\/all_states.csv' },\n      ],\n    },\n  },\n  {\n    id: 'affordability',\n    title: 'Housing Affordability Tracker',\n    desc: 'Quarterly housing affordability across U.S. metros, states, and counties \u2014 income required to buy a typical home, share of income spent on housing, share of listings affordable, and years to save a down payment, broken out by income group and down payment level.',\n    dateCol: 'period_begin',\n    noMom: true,\n    noWow: true,\n    noMetricToggle: true,\n    keyColumns: [\n      'INCOME REQUIRED ($)',\n      'SHARE OF INCOME ON HOUSING (%)',\n      'AFFORDABLE LISTING SHARE (%)',\n      'YEARS TO SAVE DOWN PAYMENT',\n      'MONTHLY HOUSING PAYMENT ($)',\n      'MEDIAN SALE PRICE ($)',\n    ],\n    startYear: 2012,\n    dateType: 'monthly',\n    badge: 'Updated Quarterly',\n    \/\/ Row-level dimension filters (client-side; applied on download). 'All Residential'\n    \/\/ and 'All' are the dataset's own aggregate rows, so they filter to a single value.\n    \/\/ Down payment has no aggregate, so its 'All Down Payments' option is a no-op (all levels).\n    dimensionFilters: [\n      { col: 'PROPERTY TYPE', label: 'Select Property Type', values: ['All Residential', 'Single Family', 'Condo\/Co-op', 'Multi-Family', 'Townhouse', 'Starter Home'], allOption: 'All Property Types' },\n      { col: 'INCOME GROUP', label: 'Select Income Group', values: ['Renter', 'Homeowner', 'White', 'Black', 'Asian', 'Hispanic'], allOption: 'All Income Groups' },\n      { col: 'DOWN PAYMENT PERCENT (%)', label: 'Select Down Payment', values: ['3.5', '5', '10', '15', '20'], allOption: 'All Down Payments', unit: '%' },\n    ],\n    geos: [\n      { type: 'country', label: 'United States', all: 'housing_affordability_tracker\/country.csv' },\n      { type: 'state',   label: 'State',         all: 'housing_affordability_tracker\/all_states.csv' },\n      { type: 'metro',   label: 'Metro Area',    all: 'housing_affordability_tracker\/top_100_metros.csv', coverageLabel: 'Top 100 most populous metro areas' },\n      { type: 'county',  label: 'County',        all: 'housing_affordability_tracker\/counties_in_top_100_metros.csv', coverageLabel: 'Counties in Top 100 Metros' },\n    ],\n  },\n];\n\n\/\/ ---------------------------------------------------------------------------\n\/\/ Constants\n\/\/ ---------------------------------------------------------------------------\nconst MONTHS   = ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec'];\nconst QUARTERS = ['Q1 (Jan\\u2013Mar)','Q2 (Apr\\u2013Jun)','Q3 (Jul\\u2013Sep)','Q4 (Oct\\u2013Dec)'];\nconst Q_MONTH  = { '01':'01', '02':'04', '03':'07', '04':'10' };\n\nconst METADATA_COLS = new Set([\n  \/\/ Snake_case names (old CSV format \/ SELECT *)\n  'period_begin','period_end','period_duration','region_type','region_type_id',\n  'table_id','is_seasonally_adjusted','region','region_name','city','state',\n  'state_code','property_type','property_type_id','parent_metro_region',\n  'parent_metro_region_metro_code','last_updated','region_id','duration',\n  'frequency','is_new_construction_transaction','source','date',\n  'rolling_12mo_sale_price_bucket','price_tier_label','population_rank',\n  'redfin_metro','series_id',\n  'snapshot_date','parent_region_type_id','parent_region_id','construction_category',\n  \/\/ Human-readable aliases (new CSV format)\n  'last updated','period begin','period end','region id','region type','region name',\n  'property type','is seasonally adjusted',\n  \/\/ Migration OD-pair dimension columns\n  'origin region name','destination region name','origin population rank',\n  'destination population rank','migration type',\n  \/\/ Migration aggregated dimension columns\n  'population rank',\n]);\nconst LABEL_OVERRIDES = {\n  ppsf: 'Price Per Sq Ft', median_ppsf: 'Median Price Per Sq Ft',\n  median_dom: 'Median Days on Market', avg_sale_to_list: 'Sale-to-List Ratio',\n  sold_above_list: 'Sold Above List', off_market_in_two_weeks: 'Off Market in 2 Weeks',\n  percent_sold_above_list_price: '% Sold Above List',\n  percent_off_market_in_two_weeks: '% Off Market in 2 Weeks',\n};\nconst PLURALS = { country: 'country', city: 'cities', county: 'counties', neighborhood: 'neighborhoods', zip: 'zips', cbsa: 'cbsas', metro: 'metros', state: 'states', census_region: 'census_regions' };\nfunction pluralize(type) { return PLURALS[type] || type + 's'; }\n\n\/\/ Geo types where MoM is not computed (sub-metro granularity \u2014 too noisy)\nconst GEO_NO_MOM = new Set(['county', 'city', 'zip', 'neighborhood']);\n\nconst GEO_DISPLAY = {\n  country:       'United States',\n  national:      'United States',\n  census_region: 'Census Regions (Northeast, Midwest, South, West)',\n  state:         'States',\n  cbsa:          'CBSAs',\n  metro:         'Metro Areas',\n  csa:           'Metro Area (CSA)',\n  county:        'Counties',\n  city:          'Cities',\n  zip:           'Zip Codes',\n  neighborhood:  'Neighborhoods',\n  price_tier:    'Price Tier',\n  property_type: 'Property Type',\n};\nfunction geoDisplay(type) { return GEO_DISPLAY[type] || type; }\n\n\/\/ Canonical display metadata per frequency key.\n\/\/ startYear is a fallback; cards override via startYears: { key: year }.\nconst FREQ_DISPLAY = {\n  monthly:     { label: 'Monthly',     dateType: 'monthly',   badge: 'Updated Monthly',   startYear: 2012 },\n  four_weeks:  { label: 'Weekly',      dateType: 'monthly',   badge: 'Updated Weekly',    startYear: 2016 },\n  quarterly:   { label: 'Quarterly',   dateType: 'quarterly', badge: 'Updated Quarterly', startYear: 2000 },\n  by_metro:    { label: 'By Metro',                  dateType: 'quarterly', badge: 'Updated Quarterly' },\n  by_category: { label: 'By Category',              dateType: 'quarterly', badge: 'Updated Quarterly' },\n  od_pairs:    { label: 'Origin-Destination Pairs', dateType: 'quarterly', badge: 'Updated Quarterly' },\n  aggregated:  { label: 'Region Summary',           dateType: 'quarterly', badge: 'Updated Quarterly' },\n  luxury:      { label: 'Luxury',      dateType: 'monthly',   badge: 'Updated Monthly', snapToPrevMonth: true },\n  non_luxury:  { label: 'Non-Luxury',  dateType: 'monthly',   badge: 'Updated Monthly', snapToPrevMonth: true },\n  both:        { label: 'Both',        dateType: 'monthly',   badge: 'Updated Monthly', snapToPrevMonth: true },\n};\n\nfunction toLabel(col) {\n  return LABEL_OVERRIDES[col] || col.split(\/[_\\s]+\/).map(w => w.charAt(0).toUpperCase() + w.slice(1)).join(' ');\n}\n\n\/\/ ---------------------------------------------------------------------------\n\/\/ Render cards\n\/\/ ---------------------------------------------------------------------------\nfunction geoTypeOptions(geos) {\n  return geos.map(g => `<option value=\"${g.type}\">${g.label}<\/option>`).join('');\n}\n\nfunction renderCard(cfg) {\n  const freqToggleHTML = cfg.freqToggle ? `\n    <div class=\"freq-toggle\">\n      ${cfg.freqToggle.map((f, i) =>\n        `<button class=\"freq-btn${i === 0 ? ' active' : ''}\" data-freq=\"${f.key}\">${f.label}<\/button>`\n      ).join('')}\n    <\/div>` : '';\n\n  const initGeos = cfg.geosByFreq ? cfg.geosByFreq[cfg.freqToggle[0].key] : (cfg.geos || []);\n\n  const comingSoonHTML = cfg.comingSoon\n    ? `<div class=\"coming-soon-notice\">&#9888; This dataset is not yet available for download. Check back soon.<\/div>` : '';\n\n  return `\n  <div class=\"card\" id=\"card-${cfg.id}\">\n    <div class=\"card-title\">${cfg.title}<\/div>\n    <div class=\"card-badge\">${cfg.badge}<\/div>\n    ${comingSoonHTML}\n    ${(cfg.keyColumns || cfg.keyColumnsByFreq) && !cfg.comingSoon ? `\n    <div class=\"metrics-info\">\n      <div class=\"metrics-info-header\">\n        <div class=\"metrics-info-label\">Download includes<\/div>\n        ${!cfg.noMetricToggle ? `<div class=\"col-mode-toggle\">\n          <button class=\"col-mode-btn active\" data-mode=\"key\">Key Metrics<\/button>\n          <button class=\"col-mode-btn\" data-mode=\"all\">All Metrics<\/button>\n        <\/div>` : ''}\n      <\/div>\n      <div class=\"metrics-pills key-metrics-pills visible\">\n        ${(cfg.keyColumnsByFreq ? cfg.keyColumnsByFreq[cfg.freqToggle?.[0]?.key] || [] : cfg.keyColumns || []).map(c => `<span class=\"metric-pill\">${c.replace(\/\\s*\\([^)]*\\)\\s*$\/, '').replace(\/\\s+(SA|NSA)\\b\/gi, '').toLowerCase().replace(\/\\b\\w\/g, l => l.toUpperCase()).replace(\/\\bFha\\b\/g, 'FHA').replace(\/\\bVa\\b\/g, 'VA')}<\/span>`).join('')}\n      <\/div>\n      ${!cfg.noMetricToggle ? `<div class=\"metrics-pills all-cols-grid\"><\/div>` : ''}\n    <\/div>` : ''}\n    ${freqToggleHTML}\n    <div class=\"form-group\">\n      <label class=\"form-label geo-type-label\">Select Geography<\/label>\n      <div class=\"select-wrapper\">\n        <select class=\"geo-type-select\"${cfg.comingSoon ? ' disabled' : ''}>\n          <option value=\"\">Select geography type...<\/option>\n          ${geoTypeOptions(initGeos)}\n        <\/select>\n      <\/div>\n    <\/div>\n    <div class=\"form-group coverage-group\" style=\"display:none\">\n      <label class=\"form-label\">Select coverage<\/label>\n      <div class=\"select-wrapper\">\n        <select class=\"coverage-select\"${cfg.comingSoon ? ' disabled' : ''}><\/select>\n      <\/div>\n    <\/div>\n    ${cfg.propertyTypes ? `\n    <div class=\"form-group\">\n      <label class=\"form-label\">Select Property Type<\/label>\n      <div class=\"select-wrapper\">\n        <select class=\"prop-type-select\"${cfg.comingSoon ? ' disabled' : ''}>\n          <option value=\"\">All Property Types<\/option>\n          ${cfg.propertyTypes.map(pt => `<option value=\"${pt}\">${pt}<\/option>`).join('')}\n        <\/select>\n      <\/div>\n    <\/div>` : ''}\n    ${cfg.dimensionFilters ? cfg.dimensionFilters.map(f => `\n    <div class=\"form-group\">\n      <label class=\"form-label\">${f.label}<\/label>\n      <div class=\"select-wrapper\">\n        <select class=\"dim-select\"${cfg.comingSoon ? ' disabled' : ''} data-col=\"${f.col}\">\n          ${f.allOption ? `<option value=\"\">${f.allOption}<\/option>` : ''}\n          ${f.values.map(v => `<option value=\"${v}\">${v}${f.unit || ''}<\/option>`).join('')}\n        <\/select>\n      <\/div>\n    <\/div>`).join('') : ''}\n    <div class=\"form-group\">\n      <label class=\"form-label\">Select Date Range<\/label>\n      <div class=\"date-range-row\">\n        <div class=\"date-col\">\n          <div class=\"date-sublabel\">Start<\/div>\n          <div class=\"date-selects\">\n            <div class=\"select-wrapper\"><select class=\"start-a\"${cfg.comingSoon ? ' disabled' : ''}><\/select><\/div>\n            <div class=\"select-wrapper\"><select class=\"start-b\"${cfg.comingSoon ? ' disabled' : ''}><\/select><\/div>\n          <\/div>\n        <\/div>\n        <div class=\"date-col\">\n          <div class=\"date-sublabel\">End<\/div>\n          <div class=\"date-selects\">\n            <div class=\"select-wrapper\"><select class=\"end-a\"${cfg.comingSoon ? ' disabled' : ''}><\/select><\/div>\n            <div class=\"select-wrapper\"><select class=\"end-b\"${cfg.comingSoon ? ' disabled' : ''}><\/select><\/div>\n          <\/div>\n        <\/div>\n      <\/div>\n    <\/div>\n    ${(cfg.keyColumns || cfg.keyColumnsByFreq) && !cfg.comingSoon ? `\n    <div class=\"col-mode-row trends-row\">\n      <span class=\"col-mode-label\">Trends<\/span>\n      <div class=\"suffix-toggle\">\n        <button class=\"suffix-btn\" data-suffix=\"none\">None<\/button>\n        <button class=\"suffix-btn active\" data-suffix=\"yoy\">YoY<\/button>\n        ${!cfg.noMom ? '<button class=\"suffix-btn\" data-suffix=\"mom\">MoM<\/button>' : ''}\n        ${!cfg.noWow ? '<button class=\"suffix-btn\" data-suffix=\"wow\">WoW<\/button>' : ''}\n\n      <\/div>\n    <\/div>` : ''}\n    <button class=\"btn-download\" disabled>${cfg.comingSoon ? 'Coming soon' : 'Download Data'}<\/button>\n    <div class=\"error-msg\"><\/div>\n    <div class=\"tiny-text\">${cfg.comingSoon ? 'Data will be available for download when this dataset launches.' : 'Downloads a filtered CSV for your selected date range'}<\/div>\n  <\/div>`;\n}\n\n\/\/ ---------------------------------------------------------------------------\n\/\/ Date pickers\n\/\/ ---------------------------------------------------------------------------\nfunction pad2(n) { return String(n).padStart(2, '0'); }\nfunction resolveStartYear(freqCfg) {\n  return freqCfg.yearsBack\n    ? new Date().getFullYear() - freqCfg.yearsBack\n    : freqCfg.startYear;\n}\n\n\/\/ Looks up the {start, end} entry for a frequency from index.json's date_ranges.\n\/\/ Multi-freq cards key by freqToggle[].key; single-freq cards key under 'default'.\nfunction dateRangeFor(cfg, freqKey) {\n  if (!cfg.dateRanges) return null;\n  return cfg.dateRanges[freqKey] || cfg.dateRanges['default'] || null;\n}\n\nfunction endOverrideFromRange(range) {\n  if (!range || !range.end) return null;\n  const yr = parseInt(range.end.slice(0, 4));\n  const mo = parseInt(range.end.slice(5, 7));\n  return (isNaN(yr) || isNaN(mo)) ? null : { year: yr, month: mo };\n}\n\nfunction startYearFromRange(range) {\n  if (!range || !range.start) return null;\n  const yr = parseInt(range.start.slice(0, 4));\n  return isNaN(yr) ? null : yr;\n}\n\nfunction initDatePicker(el, dateType, startYear, freqKey, endOverride) {\n  const now = new Date();\n  const curYear = now.getFullYear();\n  const curMonth = now.getMonth() + 1;\n  \/\/ Monthly frequency: last complete month is previous calendar month.\n  \/\/ Weekly (four_weeks) frequency: current month has data (rolling windows extend into it).\n  const isMonthlyFreq = freqKey === 'monthly' || dateType === 'monthly' || (FREQ_DISPLAY[freqKey]?.snapToPrevMonth ?? false);\n  \/\/ Cap end at the actual data extent when known, else fall back to the calendar.\n  const calendarLastMonth = isMonthlyFreq ? (curMonth === 1 ? 12 : curMonth - 1) : curMonth;\n  const calendarLastYear  = isMonthlyFreq && curMonth === 1 ? curYear - 1 : curYear;\n  const lastYear  = endOverride?.year  ?? calendarLastYear;\n  const lastMonth = endOverride?.month ?? calendarLastMonth;\n  const lastCap   = dateType === 'monthly' ? lastMonth : Math.ceil(lastMonth \/ 3);\n  const yearTop   = Math.min(curYear, lastYear);\n\n  function clampEndMonth(selA, selB) {\n    const selectedYear = parseInt(selB.value);\n    Array.from(selA.options).forEach(o => {\n      o.disabled = selectedYear === lastYear && parseInt(o.value) > lastCap;\n    });\n    if (parseInt(selA.value) > lastCap && selectedYear === lastYear) {\n      selA.value = pad2(lastCap);\n    }\n  }\n\n  ['start', 'end'].forEach(prefix => {\n    const selA = el.querySelector(`.${prefix}-a`);\n    const selB = el.querySelector(`.${prefix}-b`);\n    selA.innerHTML = '';\n    selB.innerHTML = '';\n\n    if (dateType === 'monthly') {\n      MONTHS.forEach((m, i) => {\n        const o = document.createElement('option');\n        o.value = pad2(i + 1); o.textContent = m; selA.appendChild(o);\n      });\n    } else {\n      QUARTERS.forEach((q, i) => {\n        const o = document.createElement('option');\n        o.value = pad2(i + 1); o.textContent = q; selA.appendChild(o);\n      });\n    }\n    for (let y = yearTop; y >= startYear; y--) {\n      const o = document.createElement('option');\n      o.value = y; o.textContent = y; selB.appendChild(o);\n    }\n    selA.addEventListener('change', () => checkReady(el));\n    selB.addEventListener('change', () => { clampEndMonth(selA, selB); checkReady(el); });\n  });\n\n  el.querySelector('.start-a').value = '01';\n  el.querySelector('.start-b').value = Math.min(yearTop, lastYear);\n  el.querySelector('.end-a').value = pad2(lastCap);\n  el.querySelector('.end-b').value = lastYear;\n\n  \/\/ Apply initial disabled state on the end month picker.\n  clampEndMonth(el.querySelector('.end-a'), el.querySelector('.end-b'));\n}\n\nfunction getDateRange(el, dateType) {\n  const sa = el.querySelector('.start-a').value;\n  const sb = el.querySelector('.start-b').value;\n  const ea = el.querySelector('.end-a').value;\n  const eb = el.querySelector('.end-b').value;\n  if (dateType === 'monthly') {\n    \/\/ End at the LAST day of the selected end month so weekly (four_weeks)\n    \/\/ windows beginning mid-month (e.g. period_begin 2026-05-04) aren't dropped\n    \/\/ by the period_begin <= endDate filter. period_begin is always the 1st for\n    \/\/ true monthly, so this is a no-op there.\n    const lastDay = pad2(new Date(Number(eb), Number(ea), 0).getDate());\n    return { startDate: `${sb}-${sa}-01`, endDate: `${eb}-${ea}-${lastDay}` };\n  }\n  return { startDate: `${sb}-${Q_MONTH[sa]}-01`, endDate: `${eb}-${Q_MONTH[ea]}-01` };\n}\n\nfunction getDateLabel(el, dateType) {\n  const sa = el.querySelector('.start-a').value;\n  const sb = el.querySelector('.start-b').value;\n  const ea = el.querySelector('.end-a').value;\n  const eb = el.querySelector('.end-b').value;\n  return dateType === 'monthly'\n    ? `${sb}_${MONTHS[+sa-1]}_to_${eb}_${MONTHS[+ea-1]}`\n    : `${sb}_Q${+sa}_to_${eb}_Q${+ea}`;\n}\n\n\/\/ ---------------------------------------------------------------------------\n\/\/ Card logic\n\/\/ ---------------------------------------------------------------------------\nfunction getActiveGeos(el, cfg) {\n  if (!cfg.freqToggle) return cfg.geos || [];\n  const activeKey = el.querySelector('.freq-btn.active').dataset.freq;\n  return (cfg.geosByFreq && cfg.geosByFreq[activeKey]) || cfg.geos || [];\n}\n\nfunction getActiveDateCfg(el, cfg) {\n  if (!cfg.freqToggle) return cfg;\n  const activeKey = el.querySelector('.freq-btn.active').dataset.freq;\n  return cfg.freqToggle.find(f => f.key === activeKey);\n}\n\nfunction getSelectedGeo(el, cfg) {\n  const type = el.querySelector('.geo-type-select').value;\n  return getActiveGeos(el, cfg).find(g => g.type === type) || null;\n}\n\nfunction getSelectedFile(el, cfg) {\n  const geo = getSelectedGeo(el, cfg);\n  if (!geo) return null;\n  const coverage = el.querySelector('.coverage-select').value;\n  if (coverage === 'top50' && geo.top50) return geo.top50;\n  if (coverage === 'in_top_50_metros' && geo.in_top_50_metros) return geo.in_top_50_metros;\n  return geo.all;\n}\n\nfunction checkReady(el) {\n  const cfg = getCardCfg(el);\n  if (!cfg || cfg.comingSoon) { setActive(el, false); return; }\n  const geoFile = getSelectedFile(el, cfg);\n  if (!geoFile) { setActive(el, false); return; }\n  const freqCfg = getActiveDateCfg(el, cfg);\n  const dateType = freqCfg.dateType || cfg.dateType;\n  const { startDate, endDate } = getDateRange(el, dateType);\n  const ok = startDate <= endDate;\n  setActive(el, ok);\n\n  const hint = el.querySelector('.tiny-text');\n  if (hint && ok) {\n    const years = (new Date(endDate) - new Date(startDate)) \/ (1000 * 60 * 60 * 24 * 365.25);\n    const geo = getSelectedGeo(el, cfg);\n    const coverage = el.querySelector('.coverage-select').value;\n    const isAllSmallGeo = geo && (geo.type === 'zip' || geo.type === 'neighborhood' || geo.type === 'city' || geo.type === 'county') && coverage === 'all';\n    const isNbhdInTop50 = geo && geo.type === 'neighborhood' && coverage === 'in_top_50_metros';\n    if (isAllSmallGeo) {\n      hint.textContent = `\u26a0\ufe0f Downloading all ${geoDisplay(geo.type).toLowerCase()} produces a very large file that may be slow to download and open in Excel or Google Sheets. Consider selecting \"${geoDisplay(geo.type)} in Top 50 Metros\" for a smaller file.`;\n    } else if (isNbhdInTop50) {\n      hint.textContent = `\u26a0\ufe0f Downloading neighborhoods in the top 50 metros may take a while and produce a large file. Consider narrowing your date range for a faster download.`;\n    } else if (years > 5) {\n      hint.textContent = `\u26a0\ufe0f Downloading ${Math.round(years)} years of data may produce a very large file that is slow to open in Excel or Google Sheets. Consider narrowing your date range.`;\n    } else {\n      hint.textContent = 'Downloads a filtered CSV for your selected date range';\n    }\n  }\n}\n\nfunction setActive(el, active) {\n  const btn = el.querySelector('.btn-download');\n  btn.disabled = !active;\n  btn.classList.toggle('active', active);\n}\n\nfunction getCardCfg(el) {\n  const id = el.id.replace('card-', '');\n  return CARDS.find(c => c.id === id) || null;\n}\n\n\/\/ Reads the head (header + first data row) and tail (last data row) of a CSV via\n\/\/ Range requests. CSVs are sorted by \"Period Begin\" desc, so the first row is the\n\/\/ latest week\/month and the last row is the earliest. Returns { startYear, endYear,\n\/\/ endMonth, lastUpdated } \u2014 fields are absent if not derivable. End uses Period\n\/\/ End (not Begin) so weekly rolling 4-week windows reflect the month the latest\n\/\/ row ENDED in (matches index.json's date_ranges semantics).\nasync function fetchCsvBounds(s3path) {\n  try {\n    \/\/ Two parallel Range requests: first 4KB (header + newest rows since the\n    \/\/ CSV is sorted descending by Period Begin) and last 8KB (oldest rows).\n    \/\/ RFC 7233 suffix-range `bytes=-N` skips a HEAD round-trip \u2014 the S3\n    \/\/ bucket's CORS config only whitelists GET (HEAD is rejected with 403\n    \/\/ for cross-origin requests), so a HEAD here would silently fail and\n    \/\/ the function would return empty bounds, leaving date pickers stuck\n    \/\/ on the calendar-default end month.\n    const [headResp, tailResp] = await Promise.all([\n      fetch(`${S3}\/${s3path}`, { headers: { Range: 'bytes=0-4095' }, cache: 'no-store' }),\n      fetch(`${S3}\/${s3path}`, { headers: { Range: 'bytes=-8192' }, cache: 'no-store' }),\n    ]);\n    const headLines = (await headResp.text()).split('\\n').filter(l => l.trim());\n    const tailLines = (await tailResp.text()).split('\\n').filter(l => l.trim());\n    if (headLines.length < 2 || tailLines.length === 0) return {};\n    const headers = parseCSVLine(headLines[0]);\n    const firstRow = parseCSVLine(headLines[1]);\n    const lastRow  = parseCSVLine(tailLines[tailLines.length - 1]);\n    const idx = (name) => headers.findIndex(h => h.toLowerCase().replace(\/\\s+\/g, '_') === name);\n    const beginIdx = idx('period_begin');\n    const endIdx   = idx('period_end');\n    const luIdx    = idx('last_updated');\n    const out = {};\n    if (endIdx >= 0) {\n      const fy = parseInt((firstRow[endIdx] || '').slice(0, 4));\n      const fm = parseInt((firstRow[endIdx] || '').slice(5, 7));\n      if (!isNaN(fy)) out.endYear  = fy;\n      if (!isNaN(fm)) out.endMonth = fm;\n    }\n    if (beginIdx >= 0) {\n      const ly = parseInt((lastRow[beginIdx] || '').slice(0, 4));\n      if (!isNaN(ly)) out.startYear = ly;\n    }\n    if (luIdx >= 0) out.lastUpdated = firstRow[luIdx] || null;\n    return out;\n  } catch { return {}; }\n}\n\nasync function fetchLastUpdated(s3path) {\n  try {\n    const resp = await fetch(`${S3}\/${s3path}`, { headers: { Range: 'bytes=0-8191' }, cache: 'no-store' });\n    const text = await resp.text();\n    const lines = text.split('\\n').filter(l => l.trim());\n    if (lines.length < 2) return null;\n    const headers = parseCSVLine(lines[0]);\n    const firstRow = parseCSVLine(lines[1]);\n    const col = headers.findIndex(h => h.toLowerCase().replace(\/\\s+\/g, '_') === 'last_updated');\n    if (col < 0) return null;\n    return firstRow[col] || null;\n  } catch { return null; }\n}\n\nfunction formatLastUpdated(dateStr) {\n  if (!dateStr) return null;\n  const d = new Date(dateStr + 'T00:00:00');\n  return d.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' });\n}\n\nfunction updateSuffixToggle(el, freqKey, geoType) {\n  const momBtn = el.querySelector('.suffix-btn[data-suffix=\"mom\"]');\n  const wowBtn = el.querySelector('.suffix-btn[data-suffix=\"wow\"]');\n  if (!wowBtn) return;\n  const isWeekly = freqKey === 'four_weeks';\n  const isQuarterly = FREQ_DISPLAY[freqKey]?.dateType === 'quarterly';\n  if (momBtn) momBtn.disabled = isWeekly || isQuarterly || (!!geoType && GEO_NO_MOM.has(geoType));\n  if (wowBtn) wowBtn.disabled = !isWeekly;\n  const active = el.querySelector('.suffix-btn.active');\n  if (active && active.disabled) {\n    active.classList.remove('active');\n    el.querySelector('.suffix-btn[data-suffix=\"yoy\"]').classList.add('active');\n  }\n}\n\nfunction initCard(cfg) {\n  const el = document.getElementById(`card-${cfg.id}`);\n  if (!el || cfg.comingSoon) return;\n\n  \/\/ Frequency toggle\n  if (cfg.freqToggle) {\n    el.querySelectorAll('.freq-btn').forEach(btn => {\n      btn.addEventListener('click', () => {\n        el.querySelectorAll('.freq-btn').forEach(b => b.classList.remove('active'));\n        btn.classList.add('active');\n        const freqCfg = cfg.freqToggle.find(f => f.key === btn.dataset.freq);\n\n        \/\/ Update badge then fetch Last Updated for this frequency\n        if (freqCfg.badge) el.querySelector('.card-badge').textContent = freqCfg.badge;\n        const luEl = el.querySelector('.card-last-updated');\n        if (luEl) { luEl.textContent = ''; luEl.style.display = 'none'; }\n        const freqGeosSrc = (cfg.geosByFreq && cfg.geosByFreq[btn.dataset.freq]) || cfg.geos || [];\n        const freqPillsSrc = (freqGeosSrc.find(g => g.type === 'metro') || freqGeosSrc[0])?.all;\n        if (freqPillsSrc) {\n          fetchCsvBounds(freqPillsSrc).then(bounds => {\n            const formatted = formatLastUpdated(bounds.lastUpdated);\n            if (formatted) {\n              const badge = el.querySelector('.card-badge');\n              if (badge) badge.textContent = `${freqCfg.badge} \u00b7 Last Updated: ${formatted}`;\n            }\n            if (bounds.endYear && bounds.endMonth && !dateRangeFor(cfg, btn.dataset.freq)) {\n              initDatePicker(\n                el,\n                freqCfg.dateType,\n                bounds.startYear || resolveStartYear(freqCfg) || cfg.startYear,\n                freqCfg.key,\n                { year: bounds.endYear, month: bounds.endMonth },\n              );\n            }\n          });\n        }\n\n        \/\/ Rebuild geo type dropdown and update label\n        const geoTypeSel = el.querySelector('.geo-type-select');\n        const geoTypeLabel = el.querySelector('.geo-type-label');\n        if (geoTypeLabel && freqCfg.geoLabel) geoTypeLabel.textContent = freqCfg.geoLabel;\n        let freqGeos = freqGeosSrc.slice();\n        if (freqCfg.allowedGeoTypes) freqGeos = freqGeos.filter(g => freqCfg.allowedGeoTypes.includes(g.type));\n        const placeholder = freqCfg.geoPlaceholder || 'Select geography type...';\n        geoTypeSel.innerHTML = `<option value=\"\">${placeholder}<\/option>` +\n          geoTypeOptions(freqGeos);\n\n        \/\/ Reset downstream\n        el.querySelector('.coverage-group').style.display = 'none';\n        el.querySelector('.coverage-select').innerHTML = '';\n        el.querySelector('.error-msg').style.display = 'none';\n\n        const freqRange = dateRangeFor(cfg, btn.dataset.freq);\n        initDatePicker(\n          el,\n          freqCfg.dateType,\n          startYearFromRange(freqRange) || resolveStartYear(freqCfg),\n          freqCfg.key,\n          endOverrideFromRange(freqRange),\n        );\n        updateSuffixToggle(el, btn.dataset.freq);\n        if (cfg.keyColumnsByFreq) {\n          const cols = cfg.keyColumnsByFreq[btn.dataset.freq] || [];\n          const pillsContainer = el.querySelector('.key-metrics-pills');\n          if (pillsContainer) {\n            pillsContainer.innerHTML =\n              cols.map(c => `<span class=\"metric-pill\">${c.replace(\/\\s*\\([^)]*\\)\\s*$\/, '').replace(\/\\s+(SA|NSA)\\b\/gi, '').toLowerCase().replace(\/\\b\\w\/g, l => l.toUpperCase()).replace(\/\\bFha\\b\/g, 'FHA').replace(\/\\bVa\\b\/g, 'VA')}<\/span>`).join('');\n          }\n        }\n        if (cfg.geosByFreq) {\n          const freqCountrySrc = (freqGeosSrc.find(g => g.type === 'country') || freqGeosSrc.find(g => g.type === 'metro') || freqGeosSrc[0])?.all;\n          if (freqCountrySrc) loadPills(el, freqCountrySrc, cfg.excludeColumns);\n        }\n        checkReady(el);\n      });\n    });\n  }\n\n\n  \/\/ Metrics Key\/All toggle \u2014 swaps the visible pill set AND filters the CSV\n  el.querySelectorAll('.col-mode-btn').forEach(btn => {\n    btn.addEventListener('click', () => {\n      el.querySelectorAll('.col-mode-btn').forEach(b => b.classList.remove('active'));\n      btn.classList.add('active');\n      const isKey = btn.dataset.mode === 'key';\n      el.querySelector('.key-metrics-pills').classList.toggle('visible', isKey);\n      const allGrid = el.querySelector('.all-cols-grid');\n      if (allGrid) allGrid.classList.toggle('visible', !isKey);\n    });\n  });\n\n  \/\/ Suffix toggle (multi-select; None is exclusive)\n  el.querySelectorAll('.suffix-btn').forEach(btn => {\n    btn.addEventListener('click', () => {\n      if (btn.disabled) return;\n      if (btn.dataset.suffix === 'none') {\n        el.querySelectorAll('.suffix-btn').forEach(b => b.classList.remove('active'));\n        btn.classList.add('active');\n      } else {\n        el.querySelector('.suffix-btn[data-suffix=\"none\"]').classList.remove('active');\n        btn.classList.toggle('active');\n        const anyActive = [...el.querySelectorAll('.suffix-btn:not([data-suffix=\"none\"])')].some(b => b.classList.contains('active') && !b.disabled);\n        if (!anyActive) el.querySelector('.suffix-btn[data-suffix=\"none\"]').classList.add('active');\n      }\n    });\n  });\n\n  \/\/ Geography type dropdown\n  el.querySelector('.geo-type-select').addEventListener('change', async () => {\n    const geo = getSelectedGeo(el, cfg);\n    const coverageGroup = el.querySelector('.coverage-group');\n    const coverageSel   = el.querySelector('.coverage-select');\n\n    el.querySelector('.error-msg').style.display = 'none';\n\n    if (!geo) {\n      coverageGroup.style.display = 'none';\n      checkReady(el); return;\n    }\n\n    const geoDisp = geoDisplay(geo.type);\n    let coverageOptions = `<option value=\"all\">${geo.coverageLabel || 'All ' + geoDisp}<\/option>`;\n    if (geo.top50) coverageOptions += `<option value=\"top50\">Top 50 most populous ${geoDisp.toLowerCase()}<\/option>`;\n    if (geo.in_top_50_metros) coverageOptions += `<option value=\"in_top_50_metros\">${geoDisp} in Top 50 Metros<\/option>`;\n    coverageSel.innerHTML = coverageOptions;\n    const NO_COVERAGE_TYPES = new Set(['country', 'national', 'price_tier', 'property_type']);\n    coverageGroup.style.display = NO_COVERAGE_TYPES.has(geo.type) ? 'none' : 'block';\n\n    \/\/ Reinit date picker with bounds. Prefer index.json's date_ranges (computed\n    \/\/ upstream from min\/max(\"Period Begin\")). Fall back to fetchCsvBounds only\n    \/\/ when the index lacks dates for this frequency. The CSV fetch still runs\n    \/\/ either way to pull lastUpdated for the badge.\n    const freqCfg  = getActiveDateCfg(el, cfg);\n    const dateType = freqCfg.dateType || cfg.dateType;\n    const range = dateRangeFor(cfg, freqCfg.key || 'default');\n    const bounds = await fetchCsvBounds(geo.all);\n    const endOverride = endOverrideFromRange(range)\n      || ((bounds.endYear && bounds.endMonth) ? { year: bounds.endYear, month: bounds.endMonth } : null);\n    const startYear = startYearFromRange(range)\n      || bounds.startYear\n      || resolveStartYear(freqCfg)\n      || cfg.startYear;\n    initDatePicker(el, dateType, startYear, freqCfg.key, endOverride);\n\n    const baseBadge = freqCfg.badge || cfg.badge;\n    const formatted = formatLastUpdated(bounds.lastUpdated);\n    el.querySelector('.card-badge').textContent = formatted\n      ? `${baseBadge} \\u00b7 Last Updated: ${formatted}`\n      : baseBadge;\n\n    updateSuffixToggle(el, freqCfg.key || '', geo ? geo.type : null);\n    checkReady(el);\n  });\n\n  \/\/ Coverage dropdown\n  el.querySelector('.coverage-select').addEventListener('change', () => checkReady(el));\n\n  \/\/ Date pickers\n  const initFreqCfg = cfg.freqToggle ? cfg.freqToggle[0] : cfg;\n  const initRange = dateRangeFor(cfg, initFreqCfg.key || 'default');\n  initDatePicker(\n    el,\n    initFreqCfg.dateType || cfg.dateType,\n    startYearFromRange(initRange) || resolveStartYear(initFreqCfg) || cfg.startYear,\n    initFreqCfg.key || '',\n    endOverrideFromRange(initRange),\n  );\n\n  \/\/ Disable suffix buttons that don't apply to the initial frequency\n  updateSuffixToggle(el, initFreqCfg.key || '');\n\n  \/\/ Download button\n  el.querySelector('.btn-download').addEventListener('click', () => {\n    if (localStorage.getItem('redfin_dc_tips_seen')) {\n      handleDownload(el, cfg);\n    } else {\n      showTipsModal(el, cfg);\n    }\n  });\n\n  \/\/ Metric pills + initial Last Updated. Date-picker bounds come from\n  \/\/ index.json's date_ranges; fall back to CSV-derived bounds only if the\n  \/\/ index lacks dates for this card.\n  if (cfg.pillsSrc) {\n    loadPills(el, cfg.pillsSrc, cfg.excludeColumns);\n    fetchCsvBounds(cfg.pillsSrc).then(bounds => {\n      const formatted = formatLastUpdated(bounds.lastUpdated);\n      if (formatted) {\n        const badge = el.querySelector('.card-badge');\n        if (badge) badge.textContent = `${cfg.badge} \u00b7 Last Updated: ${formatted}`;\n      }\n      const fcfg = cfg.freqToggle ? cfg.freqToggle[0] : cfg;\n      if (bounds.endYear && bounds.endMonth && !dateRangeFor(cfg, fcfg.key || 'default')) {\n        initDatePicker(\n          el,\n          fcfg.dateType || cfg.dateType,\n          bounds.startYear || resolveStartYear(fcfg) || cfg.startYear,\n          fcfg.key || '',\n          { year: bounds.endYear, month: bounds.endMonth },\n        );\n      }\n    });\n  }\n}\n\n\/\/ ---------------------------------------------------------------------------\n\/\/ Download\n\/\/ ---------------------------------------------------------------------------\nasync function handleDownload(el, cfg) {\n  const geo       = getSelectedGeo(el, cfg);\n  const s3path    = getSelectedFile(el, cfg);\n  const coverage  = el.querySelector('.coverage-select').value;\n  const errEl     = el.querySelector('.error-msg');\n  const btn       = el.querySelector('.btn-download');\n\n  const freqCfg  = getActiveDateCfg(el, cfg);\n  const dateType = freqCfg.dateType || cfg.dateType;\n  const { startDate, endDate } = getDateRange(el, dateType);\n  const dateLabel = getDateLabel(el, dateType);\n\n  errEl.style.display = 'none';\n  btn.classList.add('loading'); btn.classList.remove('active'); btn.disabled = true;\n\n  let dotCount = 0;\n  const interval = setInterval(() => {\n    dotCount = (dotCount + 1) % 4;\n    btn.textContent = 'Fetching data' + '.'.repeat(dotCount);\n  }, 400);\n\n  try {\n    const resp = await fetch(`${S3}\/${s3path}`, { cache: 'no-store' });\n    if (!resp.ok) throw new Error(\n      resp.status === 404\n        ? 'This dataset isn\\u2019t available yet \\u2014 check back soon.'\n        : `Download failed (HTTP ${resp.status}). Please try again.`\n    );\n\n    const reader  = resp.body.getReader();\n    const decoder = new TextDecoder();\n    const escCSV  = v => { const s = v == null ? '' : String(v); return (s.includes(',') || s.includes('\"') || s.includes('\\n')) ? `\"${s.replace(\/\"\/g, '\"\"')}\"` : s; };\n    let buffer     = '';\n    let headers    = null;\n    let dateCol      = -1;\n    let propTypeCol  = -1;\n    let colIndices   = null;\n    const propType   = el.querySelector('.prop-type-select')?.value || '';\n    \/\/ Generic row-level dimension filters (e.g. affordability property type \/ income group \/\n    \/\/ down payment). Empty value = no filter for that dimension.\n    const dimSelects = [...el.querySelectorAll('.dim-select')]\n      .map(s => ({ col: (s.dataset.col || '').toUpperCase(), val: s.value, idx: -1 }))\n      .filter(d => d.val);\n    const csvChunks = [];\n    let matchCount  = 0;\n    let rowCount    = 0;\n\n    const METADATA = new Set(['LAST UPDATED', 'PERIOD BEGIN', 'PERIOD END', 'FREQUENCY', 'REGION ID', 'REGION TYPE', 'REGION NAME', 'METRO', 'PROPERTY TYPE', 'INCOME GROUP', 'DOWN PAYMENT PERCENT', 'IS SEASONALLY ADJUSTED', 'PRICE BUCKET', 'BALANCE OF POWER', 'ORIGIN REGION NAME', 'DESTINATION REGION NAME', 'ORIGIN POPULATION RANK', 'DESTINATION POPULATION RANK', 'MIGRATION TYPE', 'POPULATION RANK']);\n    const CHANGE_SUFFIXES = [' YOY', ' MOM', ' WOW'];\n    \/\/ Strip a trailing \"(unit)\" tag like \"(%)\", \"($)\", \"(ppts)\" so unit-suffixed\n    \/\/ CSV headers (e.g. \"Percent All Cash (%)\") match unit-free key arrays. Also\n    \/\/ strip any `SA` or `NSA` variant word (e.g. \"Homes Sold NSA YoY (%)\" \u2192\n    \/\/ \"Homes Sold YoY\", \"New Listings SA\" \u2192 \"New Listings\") so keyColumns can stay\n    \/\/ un-suffixed even when the underlying CSV header carries an SA\/NSA marker \u2014\n    \/\/ see \"NSA suffix convention\" in HTML_EDITING_GUIDE.md.\n    const stripUnit = h => h.replace(\/\\s*\\([^)]*\\)\\s*$\/, '').replace(\/\\s+(SA|NSA)\\b\/gi, '');\n    const isChangCol = h => CHANGE_SUFFIXES.some(s => stripUnit(h.toUpperCase()).endsWith(s));\n    const changeSuffix = h => CHANGE_SUFFIXES.find(s => stripUnit(h.toUpperCase()).endsWith(s));\n\n    const colModeBtn = el.querySelector('.col-mode-btn.active');\n    const keyMode = colModeBtn && colModeBtn.dataset.mode === 'key' && cfg.keyColumns;\n    const keySet = keyMode ? new Set(cfg.keyColumns.map(k => stripUnit(k.toUpperCase()))) : null;\n\n    const activeSuffixes = new Set(\n      [...el.querySelectorAll('.suffix-btn.active')]\n        .map(b => b.dataset.suffix).filter(s => s !== 'none')\n    );\n\n    while (true) {\n      const { done, value } = await reader.read();\n      buffer += decoder.decode(value || new Uint8Array(), { stream: !done });\n      const lines = buffer.split('\\n');\n      buffer = done ? '' : lines.pop();\n\n      for (const line of lines) {\n        const trimmed = line.replace(\/\\r$\/, '');\n        if (!trimmed) continue;\n        if (!headers) {\n          headers = parseCSVLine(trimmed);\n          dateCol = headers.findIndex(h => h.toLowerCase().replace(\/\\s+\/g, '_') === cfg.dateCol.toLowerCase().replace(\/\\s+\/g, '_'));\n          propTypeCol = headers.findIndex(h => h.toUpperCase() === 'PROPERTY TYPE');\n          dimSelects.forEach(d => { d.idx = headers.findIndex(h => h.toUpperCase() === d.col); });\n          colIndices = headers.reduce((acc, h, i) => {\n            const upper = h.toUpperCase();\n            const upperStripped = stripUnit(upper);\n            if (isChangCol(upper)) {\n              if (activeSuffixes.size === 0) return acc;\n              const suf = changeSuffix(upper).trim().toLowerCase(); \/\/ 'yoy', 'mom', 'wow'\n              if (!activeSuffixes.has(suf)) return acc;\n              const baseName = upperStripped.slice(0, upperStripped.length - changeSuffix(upper).length);\n              if (keySet && !keySet.has(baseName)) return acc;\n            } else {\n              if (keySet && !keySet.has(upperStripped) && !METADATA.has(upperStripped)) return acc;\n            }\n            acc.push(i);\n            return acc;\n          }, []);\n          if (colIndices.length === headers.length) colIndices = null;\n          const outHeaders = colIndices ? colIndices.map(i => headers[i]) : headers;\n          csvChunks.push(outHeaders.map(escCSV).join(',') + '\\n');\n          continue;\n        }\n        const row = parseCSVLine(trimmed);\n        const dateOk     = dateCol < 0 || (row[dateCol] >= startDate && row[dateCol] <= endDate);\n        const propTypeOk = !propType || propTypeCol < 0 || row[propTypeCol] === propType;\n        const dimOk      = dimSelects.every(d => d.idx < 0 || row[d.idx] === d.val);\n        if (dateOk && propTypeOk && dimOk) {\n          const outRow = colIndices ? colIndices.map(i => row[i]) : row;\n          csvChunks.push(outRow.map(escCSV).join(',') + '\\n');\n          matchCount++;\n        }\n        rowCount++;\n      }\n\n      if (done) break;\n      if (rowCount % 5000 === 0 && rowCount > 0) {\n        btn.textContent = `Filtering\\u2026 ${(rowCount \/ 1000).toFixed(0)}k rows`;\n        await new Promise(r => setTimeout(r, 0));\n      }\n    }\n\n    clearInterval(interval);\n    if (!headers || matchCount === 0) {\n      throw new Error('No data found for the selected date range.');\n    }\n\n    const geoLabel = coverage === 'top50'            ? `top_50_${pluralize(geo.type)}`\n                   : coverage === 'in_top_50_metros' ? `${pluralize(geo.type)}_in_top_50_metros`\n                   : `all_${pluralize(geo.type)}`;\n    const freqLabel = freqCfg.key === 'four_weeks' ? 'weekly' : (freqCfg.key || cfg.dateType || 'monthly');\n    const colSuffix  = keyMode ? '_key_metrics' : '';\n    const ptSuffix   = propType ? `_${propType.toLowerCase().replace(\/[^a-z0-9]+\/g, '_').replace(\/_$\/, '')}` : '';\n    const dimSuffix  = dimSelects.map(d => `_${d.val.toLowerCase().replace(\/[^a-z0-9]+\/g, '_').replace(\/^_|_$\/g, '')}`).join('');\n    const filename   = `redfin_${cfg.id}_${freqLabel}_${geoLabel}${ptSuffix}${dimSuffix}${colSuffix}_${dateLabel}.csv`;\n    const blob = new Blob(csvChunks, { type: 'text\/csv;charset=utf-8;' });\n    triggerDownload(blob, filename);\n\n  } catch (err) {\n    clearInterval(interval);\n    errEl.textContent = err.message || 'Download failed. Please try again.';\n    errEl.style.display = 'block';\n  } finally {\n    clearInterval(interval);\n    btn.textContent = 'Download Data';\n    btn.classList.remove('loading');\n    checkReady(el);\n  }\n}\n\n\/\/ ---------------------------------------------------------------------------\n\/\/ CSV utilities\n\/\/ ---------------------------------------------------------------------------\nfunction parseCSVLine(line) {\n  const result = []; let cur = ''; let inQ = false;\n  for (let i = 0; i < line.length; i++) {\n    const ch = line[i];\n    if (ch === '\"') {\n      if (inQ && line[i+1] === '\"') { cur += '\"'; i++; }\n      else { inQ = !inQ; }\n    } else if (ch === ',' && !inQ) {\n      result.push(cur.trim()); cur = '';\n    } else { cur += ch; }\n  }\n  result.push(cur.trim());\n  return result;\n}\n\nfunction toCSVString(headers, rows) {\n  const esc = v => {\n    const s = v == null ? '' : String(v);\n    return (s.includes(',') || s.includes('\"') || s.includes('\\n')) ? `\"${s.replace(\/\"\/g, '\"\"')}\"` : s;\n  };\n  return [headers.map(esc).join(','), ...rows.map(r => r.map(esc).join(','))].join('\\n');\n}\n\nfunction triggerDownload(csvOrBlob, filename) {\n  const blob = csvOrBlob instanceof Blob ? csvOrBlob : new Blob([csvOrBlob], { type: 'text\/csv;charset=utf-8;' });\n  const url  = URL.createObjectURL(blob);\n  const a    = document.createElement('a');\n  a.href = url; a.download = filename;\n  document.body.appendChild(a); a.click();\n  document.body.removeChild(a); URL.revokeObjectURL(url);\n}\n\n\/\/ ---------------------------------------------------------------------------\n\/\/ Tips modal\n\/\/ ---------------------------------------------------------------------------\nlet _tipsEl = null, _tipsCfg = null;\n\nfunction showTipsModal(el, cfg) {\n  _tipsEl = el;\n  _tipsCfg = cfg;\n  document.getElementById('tips-dont-show-cb').checked = false;\n  document.getElementById('tips-dialog').showModal();\n}\n\n(function wireTipsModal() {\n  const dialog   = document.getElementById('tips-dialog');\n  const closeBtn = document.getElementById('tips-close-btn');\n  const dlBtn    = document.getElementById('tips-download-btn');\n  const cb       = document.getElementById('tips-dont-show-cb');\n\n  closeBtn.addEventListener('click', () => dialog.close());\n\n  dialog.addEventListener('click', e => {\n    if (e.target === dialog) dialog.close();\n  });\n\n  dlBtn.addEventListener('click', () => {\n    if (cb.checked) localStorage.setItem('redfin_dc_tips_seen', '1');\n    dialog.close();\n    handleDownload(_tipsEl, _tipsCfg);\n  });\n})();\n\n\/\/ ---------------------------------------------------------------------------\n\/\/ Metric pills\n\/\/ ---------------------------------------------------------------------------\nasync function loadPills(el, pillsSrc, excludeColumns) {\n  \/\/ Strip a trailing \"(unit)\" tag like \"(%)\", \"($)\", \"(ppts)\" before identity\n  \/\/ checks so the metadata\/exclude\/yoy filters work on unit-suffixed headers.\n  \/\/ Also strip any `SA` or `NSA` variant word (e.g. \"Homes Sold NSA YoY (%)\" \u2192\n  \/\/ \"Homes Sold YoY\", \"New Listings SA\" \u2192 \"New Listings\") so the All Metrics pill\n  \/\/ list collapses SA\/NSA-tagged headers to their public-facing label \u2014 see \"NSA\n  \/\/ suffix convention\" in HTML_EDITING_GUIDE.md.\n  const stripUnit = h => h.replace(\/\\s*\\([^)]*\\)\\s*$\/, '').replace(\/\\s+(SA|NSA)\\b\/gi, '');\n  const excludeSet = excludeColumns ? new Set(excludeColumns.map(c => c.toLowerCase())) : null;\n  try {\n    const resp = await fetch(`${S3}\/${pillsSrc}`);\n    if (!resp.ok) throw new Error();\n    const reader = resp.body.getReader();\n    let firstLine = '';\n    while (true) {\n      const { done, value } = await reader.read();\n      firstLine += new TextDecoder().decode(value);\n      if (done || firstLine.includes('\\n')) break;\n    }\n    reader.cancel();\n    \/\/ Always lowercase incoming headers so the filter, METADATA lookup, and\n    \/\/ toLabel re-Title-Case all behave consistently regardless of whether the\n    \/\/ CSV came from an upstream that emits UPPERCASE, Title Case, or snake_case.\n    const hdrs = firstLine.split('\\n')[0].split(',').map(h => h.trim().replace(\/\"\/g, '').toLowerCase());\n    const seen = new Set();\n    const base = hdrs.filter(h => {\n      const stripped = stripUnit(h);\n      if (!METADATA_COLS.has(stripped)\n        && !stripped.endsWith('_mom') && !stripped.endsWith('_yoy') && !stripped.endsWith('_wow')\n        && !stripped.endsWith(' mom') && !stripped.endsWith(' yoy') && !stripped.endsWith(' wow')\n        && !(excludeSet && excludeSet.has(stripped))\n        && !seen.has(stripped)) {\n        seen.add(stripped);\n        return true;\n      }\n      return false;\n    });\n    if (base.length === 0) throw new Error();\n\n    const allColsGrid = el.querySelector('.all-cols-grid');\n    if (allColsGrid) {\n      allColsGrid.innerHTML = base.map(h => {\n        const stripped = stripUnit(h);\n        const label = toLabel(stripped).replace(\/\\b\\w\/g, l => l.toUpperCase()).replace(\/\\bFha\\b\/g, 'FHA').replace(\/\\bVa\\b\/g, 'VA');\n        return `<span class=\"metric-pill\">${label}<\/span>`;\n      }).join('');\n    }\n    const allBtn = el.querySelector('.col-mode-btn[data-mode=\"all\"]');\n    if (allBtn) allBtn.textContent = `All Metrics (${base.length})`;\n  } catch {}\n}\n\n\/\/ ---------------------------------------------------------------------------\n\/\/ Init \u2014 derive display metadata synchronously (no wait), then fetch\n\/\/ index.json to override geo paths with live data.\n\/\/ ---------------------------------------------------------------------------\nconst grid = document.getElementById('grid');\n\nasync function init() {\n  \/\/ Phase 1 (sync): expand freqToggle from FREQ_DISPLAY, derive badge + pillsSrc\n  CARDS.forEach(cfg => {\n    if (cfg.freqToggle) {\n      cfg.freqToggle.forEach(f => {\n        const fd = FREQ_DISPLAY[f.key] || {};\n        f.label    = fd.label    || f.key;\n        f.dateType = fd.dateType || 'monthly';\n        f.badge    = fd.badge    || '';\n      });\n      cfg.badge = cfg.freqToggle[0].badge;\n    }\n\n    \/\/ pillsSrc: prefer country all-CSV (lowest aggregation level \u2014 finalizes\n    \/\/ earliest, so its last Period End is the most recent available data\n    \/\/ for the dataset). Falls back to metro, then any other geo. Also runs\n    \/\/ for cards without freqToggle (cash_loan, B&S) \u2014 without this, those\n    \/\/ cards skip pillsSrc, fetchCsvBounds never runs, and the picker falls\n    \/\/ through to a calendar-default end month that may not match the CSV.\n    \/\/ Used by both loadPills and fetchCsvBounds.\n    if (!cfg.pillsSrc) {\n      const firstGeos = cfg.geosByFreq\n        ? (cfg.geosByFreq[cfg.freqToggle[0].key] || [])\n        : (cfg.geos || []);\n      const geo = firstGeos.find(g => g.type === 'country')\n        || firstGeos.find(g => g.type === 'metro')\n        || firstGeos[0];\n      cfg.pillsSrc = geo?.all || null;\n    }\n  });\n\n  \/\/ Phase 2 (async): fetch index.json and override geosByFreq with live paths\n  let index = {};\n  try {\n    const resp = await fetch(`${S3}\/index.json`, { cache: 'no-store' });\n    if (resp.ok) index = await resp.json();\n  } catch {}\n\n  \/\/ Reserved (non-geo) keys at the dataset level in index.json\n  const NON_GEO_KEYS = new Set(['key_metrics', 'key_metrics_by_freq', 'trends', 'date_ranges']);\n\n  \/\/ Canonical dropdown order for geography types. Snowflake's object_construct\n  \/\/ emits keys alphabetically in JSON, so we re-sort here to match the order\n  \/\/ dashboard consumers and Suzanne's mocks expect. Unknown types fall to the\n  \/\/ bottom (rare \u2014 covered cases below should be exhaustive).\n  const GEO_ORDER = ['country', 'national', 'census_region', 'metro', 'csa', 'state', 'city', 'county', 'neighborhood', 'zip', 'price_tier', 'property_type'];\n  const geoRank = (type) => {\n    const i = GEO_ORDER.indexOf(type);\n    return i < 0 ? GEO_ORDER.length : i;\n  };\n  const sortGeos = (entries) => entries.slice().sort(([a], [b]) => geoRank(a) - geoRank(b));\n\n  \/\/ Convert an index.json geo entry to the card-config geo shape. The hardcoded\n  \/\/ entry (when present) acts as a fallback for fields index.json doesn't\n  \/\/ include \u2014 most importantly coverage_label, so a label tweak in the HTML\n  \/\/ takes effect on next page load without waiting for a publish DAG run.\n  \/\/\n  \/\/ Stale-coverage_label safeguard: when top50 comes from the hardcoded fallback\n  \/\/ (because index.json hasn't been regenerated yet to include it), drop any\n  \/\/ coverage_label from index.json. The override predates top50 being a\n  \/\/ first-class option and would otherwise alias to the same dropdown label,\n  \/\/ producing a duplicate entry. Self-heals on the next publish DAG run.\n  const toGeoEntry = (type, paths, hardcoded) => {\n    const indexPaths = typeof paths === 'object' ? paths : null;\n    const top50 = indexPaths?.top50 || hardcoded?.top50 || null;\n    const indexHasTop50 = !!indexPaths?.top50;\n    const coverageLabel = (top50 && !indexHasTop50)\n      ? (hardcoded?.coverageLabel || undefined)\n      : (indexPaths?.coverage_label || hardcoded?.coverageLabel || undefined);\n    return {\n      type,\n      label:            GEO_DISPLAY[type] || type,\n      all:              typeof paths === 'string' ? paths : paths.all,\n      top50,\n      in_top_50_metros: indexPaths?.in_top_50_metros || hardcoded?.in_top_50_metros || null,\n      coverageLabel,\n    };\n  };\n\n  let gridHTML = '';\n  CARDS.forEach(cfg => {\n    \/\/ Multi-freq cards: update geosByFreq from index.json\n    if (cfg.freqToggle && index[cfg.id]) {\n      cfg.freqToggle.forEach(f => {\n        const freqGeos = index[cfg.id][f.key];\n        if (freqGeos) {\n          const prior = cfg.geosByFreq?.[f.key] || [];\n          cfg.geosByFreq[f.key] = sortGeos(Object.entries(freqGeos)).map(([type, paths]) =>\n            toGeoEntry(type, paths, prior.find(g => g.type === type)),\n          );\n        }\n      });\n    }\n\n    \/\/ Non-toggle cards: populate geos from index.json when available\n    if (!cfg.freqToggle && index[cfg.id]) {\n      const prior = cfg.geos || [];\n      cfg.geos = sortGeos(Object.entries(index[cfg.id]).filter(([type]) => !NON_GEO_KEYS.has(type)))\n        .map(([type, paths]) => toGeoEntry(type, paths, prior.find(g => g.type === type)));\n    }\n\n    \/\/ Source-of-truth: key metrics list comes from index.json (generated by\n    \/\/ select_index_json.sql). Falls back to the hardcoded keyColumns in the\n    \/\/ card config if index.json is unavailable or doesn't include them.\n    if (index[cfg.id]) {\n      if (Array.isArray(index[cfg.id].key_metrics)) {\n        cfg.keyColumns = index[cfg.id].key_metrics;\n      }\n      if (index[cfg.id].key_metrics_by_freq) {\n        cfg.keyColumnsByFreq = index[cfg.id].key_metrics_by_freq;\n      }\n      \/\/ trends: which trend suffix buttons to render. If index.json declares\n      \/\/ the list, derive noMom\/noWow from it; otherwise fall back to the\n      \/\/ hardcoded card-config flags.\n      if (Array.isArray(index[cfg.id].trends)) {\n        const trends = new Set(index[cfg.id].trends);\n        cfg.noMom = !trends.has('mom');\n        cfg.noWow = !trends.has('wow');\n      }\n      \/\/ date_ranges: per-frequency `{start, end}` pulled from min\/max(\"Period Begin\")\n      \/\/ on each source view at index-generation time. Drives the date picker's\n      \/\/ default end (and clamp) without per-card hardcoding. Non-freq cards key\n      \/\/ this under 'default'. Falls back to fetchCsvBounds (CSV Range fetch) if\n      \/\/ absent.\n      if (index[cfg.id].date_ranges && typeof index[cfg.id].date_ranges === 'object') {\n        cfg.dateRanges = index[cfg.id].date_ranges;\n      }\n    }\n\n    gridHTML += renderCard(cfg);\n  });\n  grid.innerHTML = gridHTML;\n\n  CARDS.forEach(cfg => initCard(cfg));\n\n  if (window.location.hash) {\n  setTimeout(() => {\n    const target = document.querySelector(window.location.hash);\n    if (target) {\n      target.scrollIntoView();\n    }\n  }, 100);\n}\n\n}\n\ninit();\n<\/script>\n<\/body>\n<\/html>\t\t\t\t<\/div>\n\t\t\t\t\t<\/div>\n\t\t\t\t<\/div>\n\t\t\t\t<\/div>\n\t\t","protected":false},"excerpt":{"rendered":"<p>Redfin Data Center Download Hub Download housing market insights from across the U.S. Redfin Data Center &mdash; Downloads Housing Market Tracker Housing Market Tracker by Property Type Balance of Power: Buyers and Sellers Luxury Home Market Price Drops Home Purchase Cancellations Home Delistings &amp; Relistings Investor Home Purchases Starter Home Market Redfin Home Price Index [&hellip;]<\/p>\n","protected":false},"author":12371,"featured_media":0,"parent":24,"menu_order":0,"comment_status":"closed","ping_status":"closed","template":"","meta":{"_acf_changed":false,"site-sidebar-layout":"no-sidebar","site-content-layout":"page-builder","ast-site-content-layout":"full-width-container","site-content-style":"default","site-sidebar-style":"default","ast-global-header-display":"","ast-banner-title-visibility":"","ast-main-header-display":"","ast-hfb-above-header-display":"","ast-hfb-below-header-display":"","ast-hfb-mobile-header-display":"","site-post-title":"disabled","ast-breadcrumbs-content":"","ast-featured-img":"disabled","footer-sml-layout":"","ast-disable-related-posts":"","theme-transparent-header-meta":"default","adv-header-id-meta":"","stick-header-meta":"","header-above-stick-meta":"","header-main-stick-meta":"","header-below-stick-meta":"","astra-migrate-meta-layouts":"set","ast-page-background-enabled":"default","ast-page-background-meta":{"desktop":{"background-color":"","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""},"tablet":{"background-color":"","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""},"mobile":{"background-color":"","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""}},"ast-content-background-meta":{"desktop":{"background-color":"var(--ast-global-color-5)","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""},"tablet":{"background-color":"var(--ast-global-color-5)","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""},"mobile":{"background-color":"var(--ast-global-color-5)","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""}},"footnotes":""},"coauthors":[163],"class_list":["post-85151","page","type-page","status-publish","hentry"],"acf":[],"yoast_head":"<!-- This site is optimized with the Yoast SEO Premium plugin v24.7 (Yoast SEO v27.9) - https:\/\/yoast.com\/product\/yoast-seo-premium-wordpress\/ -->\n<title>Downloads - Redfin<\/title>\n<meta name=\"description\" content=\"Downloadable datasets covering housing market metrics from across the U.S.\" \/>\n<meta name=\"robots\" content=\"index, follow, max-snippet:-1, max-image-preview:large, max-video-preview:-1\" \/>\n<link rel=\"canonical\" href=\"https:\/\/www.redfin.com\/news\/data-center\/downloads\/\" \/>\n<meta property=\"og:locale\" content=\"en_US\" \/>\n<meta property=\"og:type\" content=\"article\" \/>\n<meta property=\"og:title\" content=\"Downloads\" \/>\n<meta property=\"og:description\" content=\"Downloadable datasets covering housing market metrics from across the U.S.\" \/>\n<meta property=\"og:url\" content=\"https:\/\/www.redfin.com\/news\/data-center\/downloads\/\" \/>\n<meta property=\"og:site_name\" content=\"Redfin Real Estate News\" \/>\n<meta property=\"article:publisher\" content=\"https:\/\/www.facebook.com\/redfin\" \/>\n<meta property=\"article:modified_time\" content=\"2026-08-06T19:58:00+00:00\" \/>\n<meta name=\"twitter:card\" content=\"summary_large_image\" \/>\n<meta name=\"twitter:site\" content=\"@redfin\" \/>\n<meta name=\"twitter:label1\" content=\"Est. reading time\" \/>\n\t<meta name=\"twitter:data1\" content=\"1 minute\" \/>\n\t<meta name=\"twitter:label2\" content=\"Written by\" \/>\n\t<meta name=\"twitter:data2\" content=\"Suzanne Harrison\" \/>\n<script type=\"application\/ld+json\" class=\"yoast-schema-graph\">{\"@context\":\"https:\\\/\\\/schema.org\",\"@graph\":[{\"@type\":\"WebPage\",\"@id\":\"https:\\\/\\\/www.redfin.com/news\\\/data-center\\\/downloads\\\/\",\"url\":\"https:\\\/\\\/www.redfin.com/news\\\/data-center\\\/downloads\\\/\",\"name\":\"Downloads - Redfin\",\"isPartOf\":{\"@id\":\"https:\\\/\\\/www.redfin.com/news\\\/#website\"},\"datePublished\":\"2026-05-05T23:13:25+00:00\",\"dateModified\":\"2026-08-06T19:58:00+00:00\",\"description\":\"Downloadable datasets covering housing market metrics from across the U.S.\",\"breadcrumb\":{\"@id\":\"https:\\\/\\\/www.redfin.com/news\\\/data-center\\\/downloads\\\/#breadcrumb\"},\"inLanguage\":\"en-US\",\"potentialAction\":[{\"@type\":\"ReadAction\",\"target\":[\"https:\\\/\\\/www.redfin.com/news\\\/data-center\\\/downloads\\\/\"]}]},{\"@type\":\"BreadcrumbList\",\"@id\":\"https:\\\/\\\/www.redfin.com/news\\\/data-center\\\/downloads\\\/#breadcrumb\",\"itemListElement\":[{\"@type\":\"ListItem\",\"position\":1,\"name\":\"Home\",\"item\":\"https:\\\/\\\/www.redfin.com/news\\\/\"},{\"@type\":\"ListItem\",\"position\":2,\"name\":\"Data Center\",\"item\":\"https:\\\/\\\/www.redfin.com/news\\\/data-center\\\/\"},{\"@type\":\"ListItem\",\"position\":3,\"name\":\"Downloads\"}]},{\"@type\":\"WebSite\",\"@id\":\"https:\\\/\\\/www.redfin.com/news\\\/#website\",\"url\":\"https:\\\/\\\/www.redfin.com/news\\\/\",\"name\":\"Redfin Real Estate News\",\"description\":\"The latest real estate news and research from technology-powered residential real estate company, Redfin.\",\"publisher\":{\"@id\":\"https:\\\/\\\/www.redfin.com/news\\\/#organization\"},\"potentialAction\":[{\"@type\":\"SearchAction\",\"target\":{\"@type\":\"EntryPoint\",\"urlTemplate\":\"https:\\\/\\\/www.redfin.com/news\\\/?s={search_term_string}\"},\"query-input\":{\"@type\":\"PropertyValueSpecification\",\"valueRequired\":true,\"valueName\":\"search_term_string\"}}],\"inLanguage\":\"en-US\"},{\"@type\":\"Organization\",\"@id\":\"https:\\\/\\\/www.redfin.com/news\\\/#organization\",\"name\":\"Redfin\",\"url\":\"https:\\\/\\\/www.redfin.com/news\\\/\",\"logo\":{\"@type\":\"ImageObject\",\"inLanguage\":\"en-US\",\"@id\":\"https:\\\/\\\/www.redfin.com/news\\\/#\\\/schema\\\/logo\\\/image\\\/\",\"url\":\"https:\\\/\\\/www.redfin.com/news\\\/wp-content\\\/uploads\\\/2020\\\/10\\\/Redfin-News-Logo.png\",\"contentUrl\":\"https:\\\/\\\/www.redfin.com/news\\\/wp-content\\\/uploads\\\/2020\\\/10\\\/Redfin-News-Logo.png\",\"width\":1100,\"height\":235,\"caption\":\"Redfin\"},\"image\":{\"@id\":\"https:\\\/\\\/www.redfin.com/news\\\/#\\\/schema\\\/logo\\\/image\\\/\"},\"sameAs\":[\"https:\\\/\\\/www.facebook.com\\\/redfin\",\"https:\\\/\\\/x.com\\\/redfin\",\"https:\\\/\\\/www.instagram.com\\\/redfinrealestate\\\/\",\"https:\\\/\\\/www.linkedin.com\\\/company\\\/redfin\",\"https:\\\/\\\/www.pinterest.com\\\/redfin\\\/\",\"https:\\\/\\\/en.wikipedia.org\\\/wiki\\\/Redfin\"]}]}<\/script>\n<!-- \/ Yoast SEO Premium plugin. -->","yoast_head_json":{"title":"Downloads - Redfin","description":"Downloadable datasets covering housing market metrics from across the U.S.","robots":{"index":"index","follow":"follow","max-snippet":"max-snippet:-1","max-image-preview":"max-image-preview:large","max-video-preview":"max-video-preview:-1"},"canonical":"https:\/\/www.redfin.com\/news\/data-center\/downloads\/","og_locale":"en_US","og_type":"article","og_title":"Downloads","og_description":"Downloadable datasets covering housing market metrics from across the U.S.","og_url":"https:\/\/www.redfin.com\/news\/data-center\/downloads\/","og_site_name":"Redfin Real Estate News","article_publisher":"https:\/\/www.facebook.com\/redfin","article_modified_time":"2026-08-06T19:58:00+00:00","twitter_card":"summary_large_image","twitter_site":"@redfin","twitter_misc":{"Est. reading time":"1 minute","Written by":"Suzanne Harrison"},"schema":{"@context":"https:\/\/schema.org","@graph":[{"@type":"WebPage","@id":"https:\/\/www.redfin.com\/news\/data-center\/downloads\/","url":"https:\/\/www.redfin.com\/news\/data-center\/downloads\/","name":"Downloads - Redfin","isPartOf":{"@id":"https:\/\/www.redfin.com\/news\/#website"},"datePublished":"2026-05-05T23:13:25+00:00","dateModified":"2026-08-06T19:58:00+00:00","description":"Downloadable datasets covering housing market metrics from across the U.S.","breadcrumb":{"@id":"https:\/\/www.redfin.com\/news\/data-center\/downloads\/#breadcrumb"},"inLanguage":"en-US","potentialAction":[{"@type":"ReadAction","target":["https:\/\/www.redfin.com\/news\/data-center\/downloads\/"]}]},{"@type":"BreadcrumbList","@id":"https:\/\/www.redfin.com\/news\/data-center\/downloads\/#breadcrumb","itemListElement":[{"@type":"ListItem","position":1,"name":"Home","item":"https:\/\/www.redfin.com\/news\/"},{"@type":"ListItem","position":2,"name":"Data Center","item":"https:\/\/www.redfin.com\/news\/data-center\/"},{"@type":"ListItem","position":3,"name":"Downloads"}]},{"@type":"WebSite","@id":"https:\/\/www.redfin.com\/news\/#website","url":"https:\/\/www.redfin.com\/news\/","name":"Redfin Real Estate News","description":"The latest real estate news and research from technology-powered residential real estate company, Redfin.","publisher":{"@id":"https:\/\/www.redfin.com\/news\/#organization"},"potentialAction":[{"@type":"SearchAction","target":{"@type":"EntryPoint","urlTemplate":"https:\/\/www.redfin.com\/news\/?s={search_term_string}"},"query-input":{"@type":"PropertyValueSpecification","valueRequired":true,"valueName":"search_term_string"}}],"inLanguage":"en-US"},{"@type":"Organization","@id":"https:\/\/www.redfin.com\/news\/#organization","name":"Redfin","url":"https:\/\/www.redfin.com\/news\/","logo":{"@type":"ImageObject","inLanguage":"en-US","@id":"https:\/\/www.redfin.com\/news\/#\/schema\/logo\/image\/","url":"https:\/\/www.redfin.com\/news\/wp-content\/uploads\/2020\/10\/Redfin-News-Logo.png","contentUrl":"https:\/\/www.redfin.com\/news\/wp-content\/uploads\/2020\/10\/Redfin-News-Logo.png","width":1100,"height":235,"caption":"Redfin"},"image":{"@id":"https:\/\/www.redfin.com\/news\/#\/schema\/logo\/image\/"},"sameAs":["https:\/\/www.facebook.com\/redfin","https:\/\/x.com\/redfin","https:\/\/www.instagram.com\/redfinrealestate\/","https:\/\/www.linkedin.com\/company\/redfin","https:\/\/www.pinterest.com\/redfin\/","https:\/\/en.wikipedia.org\/wiki\/Redfin"]}]}},"_links":{"self":[{"href":"https:\/\/www.redfin.com\/news\/wp-json\/wp\/v2\/pages\/85151","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/www.redfin.com\/news\/wp-json\/wp\/v2\/pages"}],"about":[{"href":"https:\/\/www.redfin.com\/news\/wp-json\/wp\/v2\/types\/page"}],"author":[{"embeddable":true,"href":"https:\/\/www.redfin.com\/news\/wp-json\/wp\/v2\/users\/12371"}],"replies":[{"embeddable":true,"href":"https:\/\/www.redfin.com\/news\/wp-json\/wp\/v2\/comments?post=85151"}],"version-history":[{"count":34,"href":"https:\/\/www.redfin.com\/news\/wp-json\/wp\/v2\/pages\/85151\/revisions"}],"predecessor-version":[{"id":86775,"href":"https:\/\/www.redfin.com\/news\/wp-json\/wp\/v2\/pages\/85151\/revisions\/86775"}],"up":[{"embeddable":true,"href":"https:\/\/www.redfin.com\/news\/wp-json\/wp\/v2\/pages\/24"}],"wp:attachment":[{"href":"https:\/\/www.redfin.com\/news\/wp-json\/wp\/v2\/media?parent=85151"}],"wp:term":[{"taxonomy":"author","embeddable":true,"href":"https:\/\/www.redfin.com\/news\/wp-json\/wp\/v2\/coauthors?post=85151"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}