var rb = rb || {};
            rb.language = {
   "gc_pcard_iagree" : "I agree to the",
   "questions_sent" : "Your questions have been sent to our Customer Care representatives.",
   "billing_address" : "Billing Address",
   "retake_ff" : "Back to the Foundation Finder",
   "delivery" : "Shipping",
   "form" : "Form",
   "you_have_no_orders" : "You have no orders.",
   "po_reset" : "Click here to reset and assist another customer",
   "check_bal" : "Check Balance:",
   "moisturizers" : "Moisturizer",
   "email" : "Email:",
   "your_order_summary" : "Your Order Summary",
   "confirm_cc_delete" : "Are you sure you want to delete this billing information?",
   "verify_email" : "Please verify your Email Address",
   "respect_privacy" : "By submitting your information, you agree to our <a href=\"/cms/customer_service/terms.tmpl\">Terms and Conditions</a> and <a href=\"/cms/customer_service/privacy.tmpl\">Privacy Policy</a>.",
   "gc_learn_more" : "Learn more About Gift Cards",
   "nav_favorites" : "Favorites",
   "color_finder" : "Color Search",
   "nav_item" : "Item",
   "order_confirm_your_order" : "Your order has been received and will be processed within one (1) to two (2) business   days. When your order is shipped, you'll receive an email from us with the shipping   details. Please refer to your order number whenever contacting M·A·C Cosmetics Online Service.",
   "go_shopping" : "Go shopping!",
   "two_dry_combination" : "2 - Dry Combination Skin (dry skin - mix)",
   "replenishment.service.meta_desc" : "Replenishment Service",
   "hint" : "hint",
   "cash_on_delivery" : "Cash on Delivery (¥250 commission)",
   "chars" : "NG! characters remaining",
   "screen_name_text" : "As a MAC Pro Member, you have access to the MEMBER VOICE Bulletin Board. Use the pre-selected nickname below or enter your own nickname using up to 15 characters.",
   "sign_up_mobile_email" : "Email_Address",
   "sign_in" : "Sign in",
   "security_answer" : "Answer",
   "shades" : "Shades",
   "km" : "km",
   "the_following_added_to_cart_txt" : "The following items have been added to your cart",
   "month" : "Month",
   "lips" : "Lips",
   "the_following_removed_from_cart_txt" : "The following items have been removed from your cart",
   "previous" : "Previous",
   "account_not_name" : "If you are not |first_name|, <a href=\"/account/signin.tmpl?_SUBMIT=signout\">sign out</a>.",
   "sign_up_pc_email" : "Email Address",
   "mobile_colon" : "Mobile:",
   "press" : "PRESS",
   "makeup" : "Makeup",
   "pro.required.country" : "Please enter your country matching your membership information.",
   "sms.carrier.update" : "Your mobile carrier has been updated.",
   "member_autorenew.reminder_email.subject" : "Membership Renewal Order Reminder",
   "giftwrap" : "<b>Standard Gift Wrap</b></br> Box up the must-haves...tie everything up. Give your gift some style with the M·A·C gift box!",
   "gc_chkbalance_numberlabel" : "Gift Card or eGift Card Number (16 digits)",
   "address2" : "Address 2",
   "contact_us" : "Contact Us",
   "new_customers" : "New Customers",
   "mascara" : "Mascara",
   "alt.sign-in_h.gif" : "ログイン",
   "newsletter" : "Newsletter",
   "edit_personal_info_h" : "Edit personal information and passwords",
   "shipping_total_electronic_delivery" : "Electronic delivery",
   "na" : "Not applicable",
   "deaging.name" : "Anti-Aging ",
   "in_your_cart" : "in your cart",
   "order_review.page_title" : "Order Review",
   "comments" : "A question choice is required.",
   "choose_and_review_tx" : "Please review your order and click \"Submit Order\" to complete.",
   "gift_message_desc" : "You may send a free gift message without selecting gift wrap. Your message will be printed on a separate gift card.",
   "online_order" : "Online Order",
   "shipping_information" : "Shipping Information",
   "quick_shop" : "Quick shop",
   "grooming" : "Grooming",
   "chars_remaining" : "characters remaining",
   "address" : "Address",
   "store_service" : "Service at a [BRAND] Counter or Store",
   "view_all_sizes" : "VIEW ALL SIZES",
   "select_state" : "Select state",
   "may" : "May",
   "expert_tips" : "3XPERT_TIPS",
   "special_events" : "Special Events",
   "cod_commission" : "COD Commission:",
   "ingredient_concerns" : "Concerns About [BRAND] Ingredients",
   "fragrance" : "Fragrance",
   "united_states" : "United States",
   "cod" : "COD",
   "zip" : "Zip/Postal Code",
   "shipconfirm_email_subject" : "Your MAC Online Order ::TRANS_ORDER_ID:: has shipped",
   "po_csr_signout" : "csr signout",
   "gift_message" : "Gift Message:",
   "migrated_user_do_you_accept" : "Do you accept the new legal terms?",
   "view_all" : "View all",
   "customer_email_greeting" : "customer_email_greeting.copy needed for this key",
   "sign_up_postal_code" : "Postal_Code",
   "personal_information" : "Personal Information",
   "additionalinfo" : "Additional Info:",
   "eyes" : "Eyes",
   "lipgloss" : "Lipgloss",
   "order_status_0" : "Pending",
   "status" : "Status",
   "brand" : "[BRAND]",
   "forgot_pass" : "Forgot Your Password?",
   "results_range" : "Results 1 to ::max_per_page:: of ::total_results::",
   "share" : "Share this product",
   "member_info" : "Membership Information",
   "customer_service_txt" : "If you need help or would like to order by phone, call customer service at 1-800-588-0070.",
   "option" : "Option",
   "starred_password_hint" : "*Password hint",
   "no_addresses_saved" : "You have no addresses on file.",
   "cartbox.name" : "eGift Card:",
   "ec_retrieved_email_subject" : "[Recipient Name] has received the MAC Online eGift Card you sent",
   "sets_gifts" : "Sets & Gifts",
   "shipping_instructional_text" : "There are two payment methods available on [BRAND], please pay by credit card through our payment partner or by COD. Please note that for COD there is a commission charge of ¥250 for purchase less than ¥20,000 (tax included) and ¥450 for purchase of ¥20,000 or more (tax included).",
   "select_gift_card_value" : "Select a Gift Card Value",
   "po_place_an_order" : "Click here to place an order for this customer",
   "sub_total" : "Sub Total:",
   "pores" : "Pores",
   "yes" : "Yes",
   "enter_offer_code" : "Enter offer code",
   "uneven_skin_tone" : "シミ・ソバカス",
   "delivery_every_03" : "Delivery Every 60 Days",
   "1_where_purchased" : "Where can it be purchased?",
   "pass_request_header" : "If you still do not know the password",
   "inventory_status_message_7" : "SOLD OUT",
   "on" : "on",
   "postal_code" : "Zip/Postal Code",
   "sign_in_with_pw_txt" : "We have reset your password. Please login using your temporary password and you can reset your password once signed in.",
   "shop_now" : "Shop Now",
   "order_status_1" : "Pending",
   "egiftcard_you_have_been_sent" : "YOU HAVE BEEN SENT A MAC eGIFT CARD!",
   "benefit" : "Benefit",
   "select_payment_type" : "Select Payment Type",
   "video" : "VIDEO",
   "change" : "change",
   "required.first_name" : "config Please supply your first name.",
   "description" : "Name",
   "basic_regimen" : "BASIC_REGIMEN",
   "cc_expiration_date" : "Expiration",
   "order_status_7" : "Partially Shipped",
   "once_completed_email_txt" : "Once your order is completed, you'll receive an email confirmation.",
   "member_address" : "Member Address",
   "sms.cancel.subscription.invalid" : "You have already unsubscribed from receiving SMS updates.",
   "required.password" : "config Please supply your password.",
   "migrated_user_agree_txt" : "By clicking on “I Agree” below, you are acknowledging that you have read, understand, and agree to be bound by the <a class=\"lwPopupButton\" href=\"/cms/customer_service/terms.tmpl\">Terms and Conditions</a> and <a class=\"lwPopupButton\" href=\"/cms/customer_service/privacy.tmpl \">Privacy Policy</a> for this website.",
   "required_and.sms_promotions.phone2.carrier_code.required_and.email_signup" : "Please select your mobile carrier.",
   "payment_options" : "Payment Options",
   "sign_in_forgot" : "Forgot_Password?",
   "gift_message_max_characters" : "XXX characters maximum",
   "order_status_12" : "Cancelled",
   "order_tracking_info_txt" : "To see more information, please select the order number. To find out about shipping information, click the inquiry number button, verify the delivery status.",
   "create_password_h" : "Create Password",
   "address_book_no_default_address_saved" : "This address is not on file. Please click on “Make This My Default Shipping Address” to designate one, or click on the Add New Address button above”",
   "gift_card" : "Gift Card",
   "locs_near" : "locations near you.",
   "terms_and_conditions" : "Terms and Conditions",
   "add_address" : "Add Address",
   "order_details" : "Order Details",
   "your_confirmation_txt" : "Your confirmation number is",
   "concern" : "肌の悩み",
   "powder" : "Powder",
   "customerservice" : "CUSTOMER SERVICE",
   "terms_agreement_end" : " for this website.",
   "japanpost_txt" : "Need help finding your postal code? <a href=\"http://www.post.japanpost.jp/zipcode/index.html\" target=\"_blank\">Click here</a> to look it up on japanpost.jp",
   "sign_up" : "Sign up to receive email about exclusive offers, Gifts with Purchase, in-store activities and special events.",
   "firstname" : "First Name",
   "coverage" : "Coverage",
   "email_address_verify" : "Email Address Verify",
   "color_group" : "カラーグループ",
   "katagana" : "Katagana",
   "four_very_oily" : "4 - Oily Skin",
   "add_gift_wrap" : "Gift Wrap",
   "state" : "State / Province",
   "address_2" : "Address 2",
   "express" : "Express",
   "without_tax" : "without tax",
   "temporary_password" : "Temporary password",
   "giftwrapdesc" : "When you select gift wrap, your entire order will be wrapped together.</br> Add gift wrap for US$4.95.",
   "your_default_shipping_address" : "YOUR DEFAULT SHIPPING ADDRESS",
   "articles" : "ビューティ アドバイス",
   "show_hint" : "Below is the password hint you saved with us:<br/><span class=\"error\">::PASSWORD_HINT::</span> <br/>If you still cannot remember your password, please <a href=\"javascript:signinSubmit(document.::FORM_NAME::, 'password_request');\">click here</a> and we will email it to you.",
   "security_question" : "Security Question",
   "event" : "Event",
   "birth_month" : "Birth Month",
   "conf_email-coming" : "You will receive an order confirmation within minutes by email. We also send you a shipping confirmation as soon as your order ships.",
   "pro.required.birth_month_day" : "Please enter your birth month and day matching your membership information.",
   "about_you" : "Optional Information",
   "newsletter_optin_thanks_desc1" : "Thanks!",
   "price" : "Price",
   "account_billing_choose_address" : "!Please add or choose an address at right.",
   "search_page" : "Search page",
   "finish" : "Finish",
   "ask_an_artist_external_subject" : "Ask an artist",
   "there_are" : "There are",
   "signup" : "Sign Up",
   "ship_type_description" : "This address is a:",
   "recent_inquiry_submitted" : "Thank you for submitting an inquiry regarding your password.",
   "required_dependency.sms_promotions.phone2.carrier_code.registration" : "Please review your information below and add any missing information.",
   "if_you_are_not" : "If you are not",
   "dullness" : "Dullness",
   "nav_need_help" : "Need Help",
   "gc_incl_msg_note" : "A gift message can be included during checkout.",
   "faqlink.text" : "FAQ",
   "email_us" : "Email Us",
   "default_shipping_address" : "Set as my default shipping address",
   "skin_shades" : "Shades for your skintone:",
   "no_default_payment_saved" : "This information is not on file. Click here to visit your billing page.",
   "nav_address_book" : "Address Book",
   "check_bal-js" : "Check Balance",
   "shave" : "Shave",
   "quantity" : "Quantity",
   "your_gc_num" : "Your eGift Card Number:",
   "create_an_account_here" : "Track orders, receive M·A·C Messages, write reviews, save your Faves and more when you create a My M·A·C account.",
   "question" : "Please enter a comment or message.",
   "choose_and_review_txt" : "Please review your order and click \"Submit Order\" to complete.",
   "nav_welcome_back" : "Hi, Emiko | first name |",
   "err_your_hint" : "Your password hint is:",
   "pro_newsletters" : "Pro Newsletters",
   "shipping_as_billing_address" : "Use my shipping address as my billing address",
   "subtotal" : "Sub Total",
   "egc_retrieved_message" : "has retrieved the M·A·C Cosmetics Online eGift Card that you sent.  Thanks again for your order and for visiting www.maccosmetics.com. If you  should have any question or concerns, please contact an M·A·C Cosmetics Online Customer Service representative at MACTech@maccosmetics.com.  For fastest service, please refer to  your order number in any correspondence.  Best regards, M·A·C Cosmetics Online",
   "egiftcard_footer" : "Please click here for more information  about M·A·C eGift Cards.If you have any questions, email an M·A·C Cosmetics Online Customer Service representative at MACTech@maccosmetics.com or call 1-800-588-0070. Please note that M·A·C eGift Cards are only redeemable online at www.maccosmetics.com.",
   "password" : "Password",
   "required" : "Required",
   "formula" : "Formula",
   "sign_up_fname" : "First_Name",
   "make_default_payment" : "Make this my default payment option.",
   "tax_included_above" : "(Tax Included above)",
   "products" : "Products",
   "delivery_date_dropdown_default" : "Earliest available delivery",
   "nav_items" : "Items",
   "email_signup_text" : "Sign me up to hear from M·A·C Cosmetics about future products, services, events and special insider-only offers.",
   "pending" : "pending",
   "new_password" : "New Password",
   "emailus.href" : "/templates/customer_service/contact_us.tmpl",
   "retype_password" : "Re-type Password",
   "please_select" : "Please select",
   "sun" : "Body & Suncare",
   "delete" : "Delete",
   "account_resetpw_email_donotreply" : "Please do not reply to this message. If you have received this message in error, please forward a copy of the entire message, including the headers to ::LOSTPWEMAIL:: so we can correct this matter. We apologize for any inconvenience.",
   "one_item_in_cart" : "There is 1 item in your shopping bag.",
   "nav_sign_up" : "Newsletter Sign Up",
   "concealer" : "コンシーラー",
   "ordercomplete" : "Billing",
   "alphabetically" : "Alphabetical",
   "nav_sign_in" : "Sign in",
   "items_in_cart" : "There are ::items:: items in your shopping bag.",
   "newest_info_text" : "Entitles you to the inside info on special offers and samples, plus, you'll be the first to know about the newest products and latest looks!",
   "dear" : "Dear",
   "reorder_past_purchase" : "Re-order from past purchases",
   "upgrade_flash" : "You need to upgrade your Flash Player",
   "please_not" : "Please note: At this time we can only accept US billing and shipping addresses.",
   "when_is_your_birthday" : "When is your birthday?",
   "redness" : "",
   "sms_signup_text" : "I'd like to hear from M·A·C Cosmetics Online via sms and mms. Click <a href=\"javascript:void();\" id=\"terms-popup-link\">here</a> for Mobile Terms and Conditions.",
   "choose_from_book_txt" : "Or choose an address from your address book",
   "driving_dir_h" : "Driving Directions",
   "email_alerts" : "Email alerts",
   "required_password" : "",
   "make_default_shipping" : "Make this address your default shipping address",
   "my_account_h" : "My Account",
   "err_address_identical" : "The Email Address fields must be identical.",
   "optin_confirm" : "Thank you, your submission has been received",
   "invoice_sent_to" : "Invoice sent to:",
   "alt.view-cart_btn.gif" : "VIEW CART",
   "promo_message" : "Purchase $30.00 more to qualify for FREE Standard Shipping.",
   "update" : "Change Shipping Date",
   "works_well_with_tab" : "Works well together",
   "about_mail_magazine" : "About E-mail Newsletter",
   "pass_request_contact" : "If you have any questions, please contact customer service. 1.800.588.0070.",
   "next" : "Next",
   "gc_chkbalance_remaining" : "Remaining Balance:",
   "enter_your_email" : "Enter your email, then",
   "foundation_strength" : "Neutral Undertone",
   "r_u_reg" : "Are you a Registered Member of [BRAND]?",
   "inventory_status_message_3" : "COMING SOON",
   "brows" : "アイブロー",
   "shipping_total_overnight" : "Overnight Shipping",
   "off" : "off",
   "account_landing_no_default_address_saved" : "This address is not on file. <a href = \"/account/address_book\">Click here</a> to visit your address book.",
   "orderstatus" : "Order status",
   "email_promotions" : "Yes, I would like to receive alerts from [BRAND] Online about new products and exclusive shopping offers.",
   "add_gift_wrap_label" : "Gift wrap this order. (Gift wrap charge is $3.00)",
   "enter_cc_year" : "Please supply the year when your credit card expires.",
   "edit_payment_option" : "Edit Payment Option",
   "verify_email_address_starred" : "*Verify Email Address",
   "po_email_address" : "Email Address",
   "delivery_address" : "Shipping Address",
   "starred_first_name" : "*First Name",
   "returns" : "Returns",
   "enter_email" : "In regard to verification, please input email address once more.",
   "faqlink.href" : "/cms/customer_service/faqs.tmpl",
   "migrated_user_txt" : "Please note that our <a  class=\"lwPopupButton\" href=\"/cms/customer_service/terms.tmpl \">Terms and Conditions</a> and <a class=\"lwPopupButton\"  href=\"/cms/customer_service/privacy.tmpl \">Privacy Policy</a> may have changed since you last visited this site.",
   "ask" : "Ask an Expert",
   "discount" : "Discount:",
   "order_is_not_gift_wrapped" : "Order is not gift wrapped",
   "bestseller" : "Bestsellers",
   "phone_requirements" : "<em>One phone number is required</em>",
   "aug" : "Aug",
   "benefit_priority" : "Product Features",
   "building" : "Building name:",
   "mobile_phone" : "Mobile Phone",
   "customer_service_h" : "Customer Service",
   "order_is_gift_wrapped" : "Order is gift wrapped",
   "last_name" : "Last Name",
   "shipping_total" : "Shipping",
   "estimated_total" : "Estimated Total",
   "city" : "City",
   "yes_cap" : "Yes",
   "gift_wrapped" : "Will this order be gift wrapped?",
   "free" : "Free",
   "order_history" : "Order History",
   "pro.required.postal_code" : "Please enter the zip/postal code matching your membership information.",
   "store-locator_nav" : "Stores",
   "sign_up_header" : "Sign Up now!",
   "delivery_time" : "Delivery Time",
   "enter_cc_month" : "Please supply the month when your credit card expires.",
   "nav_account_profile" : "Account Profile",
   "limited_edition" : "Limited Edition",
   "shipmethod_option_electronic_delivery" : "Electronic delivery",
   "email_gifts_instore" : "Yes, I would like to receive email about Gifts With Purchase, In-Store activities and Special Events.",
   "features" : "Features",
   "password_verify" : "Verify Password",
   "account_landing_no_default_payment_saved" : "This information is not on file. <a href =\"/account/payment_info\">Click Here</a> to visit your billing information.",
   "your_order_txt" : "Your order is not complete until you click 'Submit Order'.",
   "tax" : "Tax",
   "your_address_colon" : "Your Address:",
   "shipconfirm_footer_note" : "If your order contains backordered items, your credit card charges will reflect only the items shipped today.",
   "personal_msg" : "Gift Message",
   "delivery_every_01" : "Delivery Every 30 Days",
   "based_on" : "Based on your search criteria, there are",
   "gc_chkout_deliveredfree" : "*Delivered for free via email",
   "tracking_link" : "tracking link",
   "back_to_search" : "Back to Search",
   "account_landing_no_default_address_saved_macpro" : "This address is not on file. <a href = \"/macpro/members/address_book/index.tmpl\">Click here</a> to visit your address book.",
   "shipping_total_express" : "Express Shipping",
   "order_confirmation" : "Order Confirmation",
   "skincare" : "Skincare",
   "pc_email_address" : "Email Address",
   "nav_my_account" : "My Account",
   "checkout_billing_no_default_payment_saved" : "You have no default payment method on file. Please choose one from the dropdown menu or create a new one by clicking on Add New Payment Method.",
   "payment_method_colon" : "Payment Method:",
   "click_here" : "click here.",
   "choose" : "Choose",
   "delivery_time_dropdown_default" : "Earliest available delivery",
   "global_search" : "Search",
   "expiration" : "Expiration: [mm/yy]",
   "shipping" : "Shipping",
   "screen_name" : "MEMBER VOICE NICKNAME",
   "loading" : "Loading",
   "customer_email_salutation" : "customer_email_salutation.copy needed for this key",
   "membership_id" : "Membership ID:",
   "po_reset_customer" : "Reset customer",
   "address_book" : "Address Book",
   "select_delivery_time" : "Select a delivery time",
   "orders" : "Orders",
   "skin_type" : "Skin Type",
   "shade" : "Shade:",
   "no_favorites_message" : "You have no favorites. <br><br>To add products you like for future reference, simply click “add to favorites” button on the product page.",
   "dec" : "Dec",
   "personalized_message_txt" : "Include a personalized message with your order. Your message will be printed on a gift card. You may may send a message without selecting gift wrap.",
   "beauty_advice" : "Beauty Advice and Product Application Tips",
   "address_1" : "Address 1",
   "face" : "Face",
   "to" : "To:",
   "store_loc_h" : "Store & Event Locator",
   "group" : "home",
   "street_addr" : "Street Address:",
   "home" : "Home",
   "cartbox.email" : "E-mail address to receive",
   "country" : "Country",
   "order_status_6" : "Cancelled",
   "member_exists_signin" : "Please signin to your existing member account.",
   "payment" : "Payment Method",
   "password_starred" : "*Password",
   "forgot_pw" : "Forgot your password?<br/>Enter your email, <a id=\"forgot-password\" href=\"javascript:void();\">then click for a hint</a>.",
   "pc_colon" : "PC:",
   "start_addr" : "Please enter your starting address",
   "fields_required" : "Required",
   "card_number" : "Card Number:",
   "po_new_user" : "new user",
   "continue_shopping" : "Continue Shopping",
   "pls_choose_password" : "Please choose a password.",
   "registration_update_info" : "Please review your information below and add any missing information.",
   "edit_add" : "Edit/Add",
   "gifts" : "Gifts",
   "po_currently_signed_in_csr" : "<i>You are currently signed in with csr account</i>",
   "password_criteria_txt" : "The password is case-sensitive. Please enter 6-20 characters.",
   "new_password_verify" : "Re-enter New Password (for confirmation)",
   "most_recent_order" : "Most recent order:",
   "confirm_delete" : "Are you sure you want to delete this address?",
   "macpro_pass_request_contact" : "If you have any questions, please contact customer service. In the US, please call 1-800-866-6464; for Canadian customers, please call 1-800-387-6707 ext 8555.",
   "you_saved" : "You saved",
   "skin_concern" : "Skin Concerns",
   "or" : "Or:",
   "step1_cleanse" : "Step 1: Cleanse",
   "err_reset_password" : "Still can't remember? <a id=\"pwd-reset\" href=\"javascript:void(0);\">Click here</a> to reset your password.",
   "order_status" : "Order Status",
   "cart_override_shipping_message" : "",
   "cancel" : "Cancel",
   "address_as_delivery" : "Use this address as my delivery address.",
   "member_registration_email_subject" : "Welcome to M·A·C Pro Online",
   "how_to_use" : "Use",
   "account_landing_no_address_saved" : "This address is not on file. Click here to add it.",
   "pass_sent_header" : "Password has been sent",
   "return_to_site" : "You can return to the site and sign in at this location:",
   "benefits" : "Product Features",
   "newsletters" : "Newsletters:",
   "neutral_undertone" : "Neutral_Undertone",
   "spam" : "SPAM filters and ISPs",
   "terms_txt" : "By submitting this form, I am acknowledging that I have read, understand, and agree to be bound by the Terms and Conditions and Privacy Policy for this website.",
   "gift_options" : "Gift Options",
   "offercode" : "Offer Code",
   "enter_firstname" : "Please enter your First Name",
   "order_confirm_order_number" : "Order number",
   "example" : "example: username@network.com",
   "other_address" : "OTHER ADDRESSES",
   "err_please_sign_in" : "Please sign in below.",
   "tax_information" : "Sales Tax",
   "required_dependency.sms_promotions.email_signup" : "Please agree to the mobile terms and conditions.",
   "product_concerns" : "Concerns About [BRAND] Products",
   "add_payment_method_h" : "Add Payment Method",
   "alt.sign-in_btn.gif" : "Sign in",
   "view_past_purch" : "View All Past Purchases",
   "view_details" : "View details",
   "order_past_purchases" : "Re-order from Past Purchases",
   "nav_not_name" : "(not #|first_name|?)",
   "alt.ecard_text_how.gif" : "NOW IS A GREAT TIME TO TREAT YOURSELF TO A GIFT FROM MAC COSMETICS ONLINE.",
   "lost_pw_email_subject" : "Your Customer Service Request",
   "denotes_required_field" : "",
   "po_session_open_for" : "Session open for",
   "select_country" : "Select country",
   "cart_override_shipping_message_active" : 0,
   "print" : "Print",
   "sms.subscribe.duplicate_reference" : "You have already subscribed for SMS updates with a different account.",
   "street_block" : "Street/Block No.:",
   "business" : "Business",
   "category" : "カテゴリー",
   "alt.ecard_has_been_retrieved.gif" : "THE MAC eGIFT YOU SENT WAS RETRIEVED",
   "newsletter_optin_thanks_desc2" : "We've updated your communication preferences.",
   "locator_no_results" : "Your search returned no results.",
   "checkout_without_account" : "Wish to continue without creating an account?",
   "nav_reorder" : "Re-order from Past Purchases",
   "change_pass_text" : "We have sent you a temporary password, but in order to keep your personal information safe, please create a new password.",
   "male" : "Male",
   "account_landing_no_default_payment_saved_macpro" : "This information is not on file. <a href =\"/macpro/members/payment_info/index.tmpl\">Click Here</a> to visit your billing information.",
   "you_have_no_past_purchases" : "You have no past purchases.",
   "samples" : "Can you send me samples?",
   "bill_to" : "Bill to:",
   "requiredand.sms_promotions.phone2.carrier_code.email_signup" : "Please review your information below and add any missing information.",
   "find_stores" : "Find Stores and Events in your area",
   "female" : "Female",
   "payment_information" : "Billing Information",
   "shipmethod_option_overnight" : "Overnight",
   "pls_password_hint" : "Please enter a password hint",
   "add_new_address" : "Add New Address",
   "security_question_colon" : "Security Question:",
   "sign_up_h" : "Sign Up Now with [BRAND].com",
   "2_how_delivered" : "How will it be delivered?",
   "all" : "All",
   "err_password_identical" : "The Password fields must be identical.",
   "house_number" : "House Number",
   "customer_service" : "Customer Service",
   "shop_all_shades" : "Shop all shades",
   "account_profile" : "Account Profile",
   "bank_name" : "",
   "add_new_payment_option" : "Add New Payment Option",
   "prod_name" : "Product Name",
   "gift_wrapped_yes" : "This order will be gift wrapped.",
   "shipping_carrier" : "Shipping Carrier:",
   "po_yes" : "yes",
   "delivery_address_colon" : "Deliver to:",
   "err_verify_password" : "Confirm New Password",
   "your_cart_empty_txt" : "Your cart is currently empty.",
   "delivery_day_info" : "Have your order delivered as soon as it’s ready, or choose your delivery date.<br>Monday to Saturday, subject to availability and excluding public holidays.",
   "skin_types" : "Skin Types",
   "cert_pin" : "Certificate PIN",
   "order_status_5" : "Backordered",
   "shipmethod_option_express" : "Express",
   "remove" : "Remove",
   "migrated_user_header" : "Legal Acceptance",
   "enter_phone" : "Please enter a valid Phone Number.",
   "member_exp_date" : "Expiration Date:",
   "no" : "no",
   "sign_up_clin_emails" : "Sign me up for [BRAND] emails",
   "order" : "About my Order",
   "gift_invoice" : "Invoice",
   "mobile_email_address" : "Mobile Email Address",
   "store_loc_bc" : "Store & Event Locator",
   "limited_to_united_states" : "Addresses limited to the United States",
   "tools_gifts" : "Tools & Gifts",
   "total" : "Total",
   "po_become_customer" : "Become Customer",
   "account.migrated-user.page_title" : "Add Mobile Key",
   "gift_wrap_restrictions" : "",
   "order_date" : "Order Date",
   "shipping_address" : "Default Shipping Address:",
   "in_providing_your_email" : "In providing your email address you are agreeing to receive email communication from [Brand] Online. If you do not wish to receive such communication via email, please un-tick this box.",
   "alt.add_to_bag_btn.gif" : "Add to Cart",
   "password_criteria" : "case sensitive, 6-12 characters",
   "max_purchase_policy" : "Maximum Purchase Policy",
   "register_pro_post_form_paragraph_2" : "Post form para 2",
   "cartbox.to" : "Gift Shipping",
   "pass_need_to_reset" : "Since you have forgotten your password, we will need you to reset it.",
   "select_cc_type" : "Please select the type of credit card.",
   "search_results" : "Search Results",
   "pro_renewal_alert" : "Your M·A·C Pro account expires on [exp date]. Renew today for another year of membership.",
   "one_offer_only" : "One offer code per order",
   "address_lookup" : "Check postal code",
   "reorder-btn-instructions" : "If you would like to re-order items you have purchased in the past, click on the “Previously Purchased\" button below.",
   "info_please" : "Please take a moment to ensure that we have your most current information, so that we may continue to provide you with the best service possible.",
   "select_payment_message" : "*Select a payment method:",
   "po_start_shopping_as" : "Start shopping as:",
   "phone" : "Day Telephone",
   "tracking_number" : "Tracking Number",
   "exfoliators_masks" : "Exfoliators & Masks",
   "concerns" : "What are your main skincare concerns?<br>(check your top three concerns)",
   "pass_request_txt" : "Please click submit so that we may email you a link to reset your password. Please note, for security reasons, any credit card information you have saved with us will be deleted.",
   "product" : "Product",
   "email_sign_up_intro_copy" : "Sign up now to receive messages from [BRAND] Online! You’ll always be in the know for exciting [BRAND] news.",
   "no_items_in_cart" : "There are currently no items in your shopping cart",
   "change_pass_thanks_txt" : "You are now registered. You are now registered. You are now registered. You are now registered. You are now registered. You are now registered. You are now registered. You are now registered. You are now registered.",
   "order_status_3" : "Error with Order",
   "starred_last_name" : "*Surname",
   "required.first_name_alternate" : "config Please supply your first name informal.",
   "shipping_discount" : "Shipping Discount:",
   "customer_phone" : "Customer Phone Number",
   "terms-conditions_nav" : "Terms & Conditions",
   "still_cant_remember" : "Still Can't Remember?",
   "shopping_cart_h" : "Shopping Cart",
   "day" : "Day",
   "stores_enter" : "For the [BRAND] counters nearest you, please enter your city and state/territory or postal code.",
   "enter_city" : "Please enter your City",
   "payment_provider" : "Credit Card (Visa or Mastercard)",
   "email_address_starred" : "Email Address",
   "my_message" : "My Message or Comment",
   "signin_to_see_cart" : "If you have previously added items to your shopping cart, <a href=\"/account/signin.tmpl\">sign in</a> to see them.",
   "hazmat.item" : "<font color=\"#888888\">Cannot ship to APO/FPO address.</font>",
   "gc_chkbalance_enternumpin" : "Enter the card number and PIN below and click the check balance button to view your balance.",
   "save_results" : "Save Results to Your Profile",
   "order_gift" : "Is this a gift? YES",
   "view_bag" : "View Cart",
   "nav_orders" : "Orders",
   "purchase_one_time" : "Purchase One Time",
   "help_or_phone_order" : "If you need help or would like to order by phone call customer service at 1-800-464-8585",
   "order_confirm_email_subject" : "Thank you for placing your order at MAC Online.",
   "confirmemail" : "Re-type Email Address",
   "mobile_opt_in_email_signup" : "M&middot;A&middot;C goes mobile! Share your cell phone number to receive updates.",
   "password_hint" : "Password Hint",
   "po_registered_user" : "registered user",
   "recruiting" : "recruiting.copy needed for this key",
   "pc" : "(PC)",
   "size" : "Size",
   "do_not_save_credit_card" : "I do not want to save my credit card details",
   "signout" : "Sign Out",
   "mobile_operator" : "Service Provider",
   "ec_activation_email_subject" : "The eGift Card you purchased at MAC Cosmetics Online has been sent to Recipient. Below is a copy for your records.",
   "exp_date" : "",
   "faq" : "Frequently Asked Questions.",
   "enter_password" : "Enter Password",
   "change_pass_header" : "Password recovery",
   "shipping_method" : "Shipping Method",
   "po_no" : "no",
   "added_to_shopping_bag" : "was added to your shopping bag.",
   "yes_newsletter_txt" : "I'd like to hear from M·A·C Cosmetics Online about products, services, events and special insider-only offers.",
   "member_autorenew.error_email.reminder_error.subject" : "MAC PRO AUTO-RENEW REMINDER: Please Update Your Account",
   "print_complete_order" : "Print Order Details",
   "sms.subscribe.subscription.already_active" : "You are already subscribed.",
   "customer_email_we_received" : "customer_email_we_received.copy needed for this key",
   "pin" : "Certificate PIN",
   "notsure" : "Not Sure",
   "cert_num" : "Certificate Number",
   "valid_cc" : "Please note: A valid credit card is required when using an eGift Card. If your order total costs less than the balance of your eGift Card, your credit card will not be charged.",
   "inventory_status_message_2" : "Out of stock - ships soon!",
   "enter_zip" : "Enter zip",
   "yes_mobile_newsletter_txt" : "Yes! I would like to receive a mobile newsletter about products, online offerings, and events.",
   "bronzer" : "bronzer",
   "gc_purchase_with_giftcard" : "Amount charged to gift card",
   "womens" : "レディース",
   "requiredfields" : "Denotes required fields",
   "enter_lastname" : "Please enter your Last Name.",
   "no_dashes_spaces" : "No spaces or dashes",
   "enter_cc" : "Please enter your credit card number with no spaces or dashes",
   "deaging" : "Anti-Aging",
   "order_review" : "Order Review",
   "other" : "Other",
   "customer_email_thankyou" : "customer_email_thankyou.copy needed for this key",
   "cc_charge" : "Amount charged to credit card",
   "customer_name" : "Customer Name",
   "delivery_every_02" : "Delivery Every 45 Days",
   "contact_phone" : "Contact Phone Number",
   "store_loc_title" : "Stores & Events > [BRAND]",
   "pro_international_checkout" : "Please note that international Pro members cannot purchase online. Please visit your local store to purchase M·A·C products. <a href=\"/locator/index.tmpl\">Find a store near you</a>.",
   "account_billing_no_default_payment_saved" : "This information is not on file. Please click on “Make This My Default Payment Method” to designate one, or click on the Add New Payment Method button above.",
   "alt.sign-up_btn.gif" : "登録する",
   "kangi_kana_ascii" : "Kanji / Kana / ascii",
   "gc_pcard_terms" : "Gift Cards Terms and Conditions.",
   "one_very_dry_to_dry" : "1 - Very Dry to Dry Skin",
   "checkout_shipping_no_default_address_saved" : "You have no default address on file. Please choose an address from the address book or create a new one by clicking on Add New Address.",
   "shippinginfo" : "Shipping & Handling",
   "select_2nd_day" : "Please select Second Day as your shipping method.",
   "order_status_4" : "Delivered",
   "err_enter_address" : "Please enter your address.",
   "gc_chkbalance_pinlabel" : "Gift Card or eGift Card PIN (8 digits)",
   "your_email" : "Your email*",
   "signin_pro_clicktoregister" : "click here to register.",
   "items" : "items",
   "payment_info" : "Billing Information",
   "select_default_store" : "My M·A·C Store",
   "three_oily" : "3 - Combination Oily Skin",
   "add_to_favorites" : "Add to Favorites",
   "lookup_address" : "Lookup Address",
   "shipmethod_option_second_day" : "Second Day",
   "delivery_every_06" : "Delivery Every 120 Days",
   "order_status_2" : "Shipped",
   "shipping_total_second_day" : "Second Day Shipping",
   "your_address" : "Address",
   "request" : "Request",
   "create_an_account" : "Create an Account",
   "pls_confirm_password" : "Please confirm your password.",
   "cartbox.message" : "Gift Message",
   "account_resetpw_email_save_msg" : "Please save this message for future reference. For your security, please note that when you sign in, you will be prompted to reset your password. Any stored credit card information will be deleted. If you need assistance, please call customer service at 1-800-588-0070.",
   "your_sex" : "Your sex",
   "name" : "Name:",
   "phone_2" : "Evening Telephone",
   "terms_agreement_and" : " and ",
   "details" : "details",
   "customer_email_will_review" : "customer_email_will_review.copy needed for this key",
   "egift_check_txt_04" : "eGift Card PIN (8 digits)",
   "card_type" : "Card Type:",
   "no_past_purchases" : "You have no past purchases",
   "purchase_with_egift" : "Purchase with eGift Card",
   "pass_sent_txt" : "E-mail has been sent to the address below to let you know the temporary password. Next time you log in with your temporary password, please set a new password.",
   "shipping_total_standard" : "Standard Shipping",
   "change_pass_thanks_header" : "You have reset your password",
   "inquiry_regarding" : "Questions",
   "no_items" : "There are currently no items in your shopping cart",
   "po_phone_order" : "Phone Order",
   "cl_online_order" : "My [BRAND] Online Order",
   "privacy_policy" : "Privacy Policy",
   "screenname" : "Screenname:",
   "mobile" : "(Mobile)",
   "select_one" : "Choose one",
   "offers" : "Promotions",
   "complete_form" : "Can't find an answer within the FAQ's above? We'd like to hear from you! Complete and submit the form below.",
   "security_answer_colon" : "Security Answer:",
   "sign_up_email" : "SIGN UP FOR EMAIL",
   "alerts" : "Alerts:",
   "in_your_bag" : "in your shopping bag.",
   "verify_password" : "Verify Password",
   "order_not_gift" : "Is this a gift? NO",
   "checkout_guest" : "Click here to checkout as a Guest.",
   "your_default_payment_info" : "YOUR DEFAULT PAYMENT METHOD",
   "edit_update_shipping" : "Edit/Update Shipping",
   "no_favorites_signin" : "If you are a member of [BRAND] Online and you have saved favorites, <a href=\"/account/index.tmpl\">click here</a> to sign in and see them.",
   "read_reviews" : "Read reviews",
   "po_phone_number" : "Phone Number",
   "err_verify_email" : "Please verify your email address.",
   "reenter_password" : "Please re-enter your password",
   "fields_are_required" : "Denotes required fields",
   "sign_up_clin_store" : "Yes, I would like to receive email about Gifts with Purchase, In-Store activities and special events.",
   "order_num" : "If available please provide your order no.",
   "verify_email_address" : "Verify Email Address",
   "optin_error" : "Sorry, there was an error submitting your request.",
   "delivery_every_05" : "Delivery Every 90 Days",
   "web_site" : "Questions about our Web site",
   "search_again" : "Search again",
   "email_macpro_signup_text" : "M·A·C Pro Emails<br> I'd like to hear from M·A·C PRO Membership about my membership, M·A·C collections, M·A·C PRO events, M·A·C PRO Online and special Pro Member-only offers.",
   "email_address" : "Email Address",
   "foundation_desc" : "Stay True Make up<br />Oil Free Formula.",
   "checkout_message_instructions" : "* PLEASE NOTE: English language characters are accepted only. Use of other language characters cannot be accepted and will result in processing delays.",
   "newest" : "Newest",
   "save_message" : "Please save this message for future reference.",
   "earliest_possible" : "Earliest available delivery",
   "gc_chkout_redemptionamt" : "Gift Card Redemption Amount",
   "edit_this_address" : "Edit this address",
   "password_or_reg" : "My [BRAND] Registration or Password",
   "signin_pro_registration" : "If you are a M·A·C PRO member who has previously signed in before on M·A·C or M·A·C Pro sites, please sign in under \"Already Registered\" with your email and password. If you are a member and would like to register to receive your special PRO privileges",
   "egiftcard_sender_copy" : "The M·A·C eGift Card you purchased at http://www.maccosmetics.com has been sent to ::GIFTCARD_TO::. Below is a copy for your records.",
   "colour_group" : "C0lour_Group",
   "cm_opt_anon_status" : "cm_opt_anon_status.copy needed for this key",
   "returning_customers_checkout_signin" : "Have a My M·A·C, M·A·C Pro account or written a review before? Existing Customers and Pro members, sign in with your email address and password here.",
   "all_shades" : "All Shades",
   "pro_discount" : "Pro discount is available on select items",
   "lastname" : "Last Name",
   "close" : "Close",
   "become_member" : "Become a member of [ BRAND ] Online!",
   "shipconfirm_email_heading" : "YOUR ORDER FROM M·A·C COSMETICS ONLINE HAS SHIPPED",
   "mail_magazine" : "About E-mail Newsletter",
   "select" : "Please select",
   "po_reset_are_you_sure" : "Are you sure you want to sign out as this customer?",
   "store" : "Store:",
   "shipped" : "shipped",
   "enter_state" : "Please select the state for this address.",
   "delivery_options" : "Delivery Method",
   "zipcode" : "Postal Code",
   "thank_you" : "Thank you",
   "residence" : "Residence",
   "message" : "Please limit the length of the giftcard message to 215 characters.",
   "shop" : "Shop",
   "cc_number" : "Credit Card Number:",
   "last_ordered" : "Last ordered",
   "deliver_to_different_address" : "Deliver To A Different Address",
   "first_name" : "First Name",
   "chemistry" : "chemistry",
   "shipmethod_option_standard" : "Standard",
   "nav_payment_info" : "Billing Information",
   "sign-up_intro_copy" : "You’ll enjoy a more personalized experience with each visit&mdash; and be able to save your custom-fit recommendations, too.",
   "golden_undertone" : "Golden_Undertone",
   "account_landing_no_payment_saved" : "This information is not on file. Click here to add it.",
   "add_new_payment_option_h" : "Add Payment Method",
   "no_address_lookup" : "There is no addresses found.",
   "passwordhintdesc" : "Enter a question or phrase that will help remind you of your password.",
   "thank_you_for_your_order" : "Thank You for Your Order",
   "order_number" : "Order Number",
   "promocode" : "Promotional Code",
   "passwordhint" : "Password Hint",
   "sort_by" : "Sort by",
   "tools" : "Tools",
   "customer_service_txt_pro" : "If you need help or would like to order by phone, call customer service at 1-800-866-6464.",
   "billing_address_header" : "BILLING ADDRESS",
   "mobile_opt_in_terms" : "I'd like to hear from M·A·C Cosmetics Online via sms and mms. I agree with the <a href=\"javascript:void();\" id=\"terms-popup-link\">Mobile Terms and Conditions.</a>",
   "foundation" : "Foundation",
   "no_cap" : "NO",
   "gift" : "Gift",
   "gift_wrapped_no" : "This order will not be gift wrapped.",
   "returning_customers" : "Returning Customers",
   "required.last_name_alternate" : "config Please supply your last name informal.",
   "welcome" : "Welcome,",
   "prefecture" : "Prefecture:",
   "checkout_billing_choose_address" : "Please choose your billing address.",
   "retrieve_egiftcard_now" : "RETRIEVE YOUR EGIFT CARD NOW!",
   "signin_return_customer" : "Have a My M·A·C, M·A·C Pro account or written a review before? Existing Customers and Pro members, sign in with your email address and password here.",
   "describe_skin_tone" : "How would you describe your skin tone?",
   "nav_sign_out" : "Sign Out",
   "customer_type" : "I am a:",
   "select_delivery_date" : "Select a delivery day",
   "edit" : "Edit",
   "pink_undertone" : "Pink_Undertone",
   "required_dependency.pc_email_promotions.email_signup" : "Please check the email box.",
   "your_password_is" : "Your temporary password is:",
   "delivery_date" : "Delivery Date",
   "ask_an_artist_internal_subject" : "Ask an artist for",
   "from" : "From:",
   "shade_name" : "Shade name:",
   "order_total" : "Order Total:",
   "gender" : "Gender",
   "oct" : "Oct",
   "shipconfirm_email_intro" : "We are pleased to inform you that your order ::NUMBER:: has been processed and shipped. For your convenience, we are enclosing the details of your order.",
   "required.customer_type.contact_us" : "Please tell us if you are a U.S., Canadian or Pro customer.",
   "shipconfirm_email_tracking" : "The tracking number is:",
   "signin" : "Sign In",
   "invoices_will_be_sent" : "Note: invoices for gifts will be sent to this address.",
   "register_pro_post_form_paragraph_1" : "Post form para 1",
   "required.last_name" : "config Please supply your last name.",
   "filter_by" : "Filter by",
   "prod_questions" : "Product Questions",
   "re-enter_password_starred" : "*Re-enter Password",
   "edit_address_h" : "Edit address",
   "more" : "More",
   "year" : "Year",
   "pro_advice" : "Professional Beauty Advice and Makeup Tips",
   "expires" : "Exp. Date",
   "billing" : "Billing",
   "meet_your_match" : "conf Meet Your Match!<br/>Here's the foundation we recommend for your skin.",
   "birth_day" : "Birth Day",
   "create_an_account_h" : "Create an Account",
   "add_to_bag" : "Add to Cart",
   "frequency" : "Frequency",
   "gift_invoice_desc" : "Your gift invoice will be sent to the following address:",
   "deliveredforfree" : "*Delivered for free via email",
   "giftcard" : "Redeem a Gift Card",
   "delivery_and_options" : "Delivery and Options",
   "foundation_tone" : "Moderately Fair",
   "my_account" : "My Account",
   "regarding" : "My Question is regarding",
   "for" : "for:",
   "mar" : "Mar",
   "skin_tone" : "Skin Tone",
   "payment_method" : "Payment Method",
   "city_town" : "City",
   "address1" : "Address 1",
   "member_autorenew.order_confirm_email.subject" : "Membership Renewal Order Confirmation",
   "edit_address" : "Edit Address",
   "when_is_birthday" : "Please enter your birth date (required)",
   "terms_agreement_start" : "By submitting this form, I am acknowledging that I have read, understand, and agree to be bound by the ",
   "po_signin_instructions" : "<b>INSTRUCTIONS</b><br>Enter the customer's email address in the \"Email Address\" field below.<br><br>If customers insist they do not have an email address or they do not want to give it over the phone, please proceed with the following:<br> <li> Ask whether they have ordered on the site (via phone order) before.<br> <li>If they are first-time buyers, please ask for their contact phone number, and enter it in the \"Phone Number\" field below.  Upon submitting, a <u>non-working email address</u> will be created with that phone number for the purposes of creating an account.<br> <li>If they have bought before, they may have provided a phone number, and an account was created with that purchase.  Please ask for the phone number, but try to find that account in .NET (enter the phone number with no spaces and end with a % in the email search field and submit).  It is preferable that the same account is used.  Please find the full email address that was created and enter it in the \"Email Address\" field. If the email address cannot be found, then use the phone number. <i>Note that the \"Phone Number\" field is a last resort.</i><br>",
   "dry_combination" : "2 - Dry Combination Skin (dry-combination skin)",
   "edit_address_book_link" : "Click here to change your address.",
   "hazmat_note" : "Please note that a signature may be required upon delivery for Overnight or 2nd Day shipments.",
   "help" : "Help",
   "in_store_service" : "In-Store Service",
   "member_autorenew.error_email.cancelled.subject" : "Membership Renewal Order Cancellation",
   "return_to_site_sign_in" : "You can return to the site and sign in at this location:",
   "required.password2" : "config Please confirm your password.",
   "works_well_with" : "Works Well With",
   "to_reorder_click_add_btn" : "To re-order click the Add to Bag button.",
   "alt.your_skin_consult" : "Your Skin Consultation",
   "undertone" : "not sure of your undertone?",
   "enter_email_checkout_signin" : "Start by entering your email address, and we’ll walk you through the checkout process.",
   "email_alerts_txt" : "I would like to receive alerts from [BRAND] about new products and exclusive offers.",
   "po_currently_signed_in" : "You are currently signed in as",
   "qty" : "QTY",
   "delivery_every_04" : "Delivery Every 75 Days",
   "sep" : "Sep",
   "customerservice.landing.name" : "Contact Us"
};
        
            rb.brand = {
   "mac_aids_fund_copy" : "Supporting men, women, and children affected by HIV/AIDS globally, the M·A·C AIDS Fund was established by M·A·C...",
   "pro_acct_sitechange_usca" : "As a registered Pro Member, you will have access to view and shop all M·A·C Products, including Pro Products, using your discount here at M·A·C Online.  <br><br>To access Pro exclusive content including artistry, trends, Pro events and Pro Membership details, <a href=\"/macpro/\" target=\"_blank\">visit macpro.com</a>.",
   "alt.pnav_security_off.gif" : "Security",
   "alt.h_creative_influence.gif" : "Creative Influence",
   "packaging_4" : "Packaging No.4",
   "waitlist_please_enter_valid_email" : "Please enter your email address in the following format: jane@aol.com",
   "chrs_remaining_singular" : "character remaining",
   "alt.h_search_results_430x28.gif" : "Search Results",
   "corporate_limits" : "For corporate gift purchases and services that exceed our maximum purchase policy, please call 1-800-588-0070.",
   "country_it" : "Italy",
   "btm_return_packaging11" : "Empty Lipstick Tube",
   "alt.mac_stores.gif" : "MAC Stores",
   "alt.department_stores.gif" : "Department Stores",
   "alt.h_fav_mac_prod_prod.gif" : "Favourite M·A·C Product (M·A·C Pro)",
   "alt.btn_view_collectin.gif" : "View Collection",
   "locator_no_results" : "Your search returned no results.",
   "lipstick_amplified_creme" : "Ultra-creamy. Quietly shiny. Colour-packed! Hi-res: hi-impact.",
   "no_previously_purchased_items" : "No previously purchased items.",
   "prolanding_student_membership" : "Support for the Makeup Professional comes in many ways at M·A·C and the M·A·C PRO Student Program is just the beginning of our relationship with any new Makeup Professional. It is ONLY open to students attending a school or institution of Makeup Artistry, Cosmetology, Hairstyling, Aesthetics, Photography, Fashion Styling or Performing Arts. This paid-for membership program offers students a direct link to the M·A·C world as well as member-exclusive benefits. M·A·C PRO Student Members must attend an eligible school or institution of Makeup Artistry or the Performing Arts. It's the beginning of M·A·C's dynamic relationship with any makeup professional just starting out.<br><br> <b>How do I become a M·A·C PRO Student Member?</b><br> For information about program eligibility and details about membership benefits please visit <a href=\"/macpro/\"><font color=\"#FFFFFF\">M·A·C PRO</font></a> or contact 1-877-553-5536 (available Monday through Friday from 9:00am to 5:00pm) or email <a href=\"mailto:promembership@maccosmetics.com\"><font color=\"#FFFFFF\">M·A·C PRO Membership</font></a>.",
   "powder_blush_matte" : "Flat matte finish. Goes on lightly. Builds well. Blends with the skin.",
   "describe_question" : "Please, describe your question or concern in as much detail as possible.",
   "hair_color" : "Hair Colour",
   "gc_check_gc_balance" : "Checking balance on M·A·C Gift Cards",
   "dropdown_mac_store" : "Service at a M·A·C Counter or Store (address required)",
   "alt.pro_stores.gif" : "PRO Stores",
   "10_backordered_items" : "Backordered Items",
   "5_recycling" : "Recycling with Back To MAC",
   "gnav_items_in_bag" : "items",
   "alt.viva_glam_products.gif" : "VIVA GLAM Products",
   "alt.h_my_fav_products.gif" : "My Favorite Products",
   "alt.btn_mascara_finder.jpg" : "Mascara Finder",
   "added_to_faves" : "was added to your faves.",
   "pink_beige" : "Pink Beige",
   "of" : "of",
   "alt.btn_next_step.gif" : "Print Form & Next Step",
   "powder_blush_frost" : "Iridescent, lightly shimmering colour. Adds highlights: provides a frosted \"bloom\" to the cheeks.",
   "alt.pnav_asia_australia_off.gif" : "ASIA & AUSTRALIA",
   "alt.btn_view_location_results.gif" : "View Location Results",
   "start_address" : "start address",
   "previous" : "Previous",
   "alt.pnav_top_inquiries_off.gif" : "Top Inquiries",
   "alt.pnav_chat_live_200x12_off.gif" : "Chat Live with an Artist",
   "blonde" : "Blonde",
   "trendy" : "Trendy",
   "like_to_hear_from_mac" : "I’d like to hear from M·A·C Cosmetics Online about products, services, events and special insider-only offers.",
   "registered_user_pro" : "YES, I AM A REGISTERED M•A•C PRO USER",
   "makeup_services_ays" : "A product demonstration introducing you to some of  the product techniques that can add to, and enhance your...",
   "registered_user" : "YES, I'M A REGISTERED M·A·C USER",
   "pro_acct_renewal_amount" : "Amount",
   "btm_return_packaging4" : "Empty Cream Colour Base",
   "pro_acct_renewal_info" : "Auto renewal keeps your membership current and allows   you to continue to enjoy the benefits of your M·A·C   PRO membership without interruption. You can choose to   be charged the annual fee each year automatically when   your membership expires.",
   "alt.pnav_shipping_off.gif" : "Shipping",
   "alt.h_emailusquestion.gif" : "Email M·A·C",
   "alt.gnav_whats_new_off.gif" : "What’s New",
   "alt.pnav_returns_exchanges_off.gif" : "Returns & Exchanges",
   "was_added_to_your_favourites" : "was added to your favourites.",
   "country_tw" : "Taiwan",
   "search_site" : "Search the site",
   "pro_acct_membership_expire" : "Your membership will expire on:",
   "alt.h_finish.gif" : "Finish",
   "search_no_results" : "Your search for \"QUERY\" returned no results.",
   "makeup_services_lips" : "A production demonstration in which the classic  neutral lip or more dramatic lip is created. Defining techniques and how  to use colour.",
   "shoptogether_landingpage_meta_title" : "Try ShopTogether on MAC",
   "alt.pnav_featured_goodbyes_off.gif" : "Featured Goodbyes",
   "alt.h_discontinued_prods.gif" : "Goodbyes",
   "member_exp_date" : "Expiration Date:",
   "alt.pnav_viva_glam_off.gif" : "VIVA GLAM",
   "alt.pnav_shipping_information_off.gif" : "Shipping Information",
   "3_careers_artist" : "Careers As a MAC Artist",
   "alt.fnav_legal_off.gif" : "Legal/Privacy",
   "alt.h_issue.gif" : "Issue or Question",
   "dark_brown" : "Dark Brown",
   "country_za" : "South Africa",
   "alt.pnav_programs_services_off.gif" : "Programs and Services",
   "red" : "Red",
   "isp_msg" : "PLEASE NOTE: Your Internet Service Provider (ISP) or email client may be using filters to block email that may affect the receipt of email from maccosmetics.com. To be sure that our response reaches you, please consult the help section of your email program or contact your ISP directly to see how to relax or remove the settings that filter or block email. This will ensure that correspondence from maccosmetics.com will be accepted. Additional Information is available in our FAQ section above. If you have further questions, please contact us at 1-800-588-0070.",
   "wait_for_operator_to_respond" : "Please wait for a site operator to respond.",
   "dramatic" : "Dramatic",
   "alt.h_list_your_preferred_shades.gif" : "List Your Preferred Shades",
   "unsubscribe_email_address" : "Email Address",
   "gc_redeeming_gc_at_store" : "Redeeming M·A·C Gift Cards at  MAC Retail Store",
   "event_name" : "Event Name",
   "blushcreme_cream" : "Soft and creamy. Brings a vibrant, dewy fresh, slightly shiny look to the cheeks. Blends well, layers exquisitely. ",
   "help_or_order" : "If you need help or would like to order by phone, call customer service at 1.800.588.0700.",
   "alt.pnav_my_messages_off.gif" : "My Messages",
   "packaging_3" : "Packaging No.3",
   "alt.h_top_searches.gif" : "Top Searches",
   "chat_live_artist_note" : "If you wish for an immediate response and it is between 10 am and 10 pm EST, Live Chat with a MAC Cosmetics Online Makeup Artist.",
   "email_an_artist_form_note" : "The M·A·C attitude is expressed by our artists, seen and felt in our stores and accessed online. Email us to get answers to your questions.",
   "dark" : "Dark",
   "macpro" : "M·A·C Pro",
   "fair" : "Fair",
   "lipstick_lustre" : "Demi-sheer with wet-look lustre finish. Very slick. Makes lips look soft, smooth, ultra-moist.",
   "full_pan" : "Your palette is full. Remove a color by dragging swatch off palette or clicking an active swatch below.",
   "submit" : "Submit",
   "alt.pnav_nth_america_off.gif" : "NORTH AMERICA",
   "pro_acct_renewal_transaction" : "Transaction date",
   "item_in_favourites" : "item in favourites",
   "search_results" : "Your search for \"QUERY\" returned RESULTS <span>result(s)</span>.",
   "alt.fnav_live_chat_off.gif" : "Live Chat",
   "11_returns" : "Returns and Exchanges: Damaged Items",
   "alt.gnav_newsworthy_off.gif" : "Newsworthy",
   "somkey_eyes" : "Smokey Eyes",
   "register_pro_member_id" : "11 digit Pro Membership ID (12#########)",
   "member_info" : "Membership Information",
   "dropdown_another_issue" : "Another issue",
   "alt.shipping_address_off.gif" : "Shipping Address",
   "gc_redeeming_gc_by_phone" : "Redeeming M·A·C Gift Cards at  1-800-588-0070",
   "btm_co" : "c/o GSI Fulfillment",
   "unsubscribe_unsubscribe" : "Unsubscribe email from M·A·C Cosmetics Online.",
   "makeup_services_fla" : "A demonstration to custom fit your new MAC Lashes.  Includes style consultation/selection, fitting, trimming and positioning  of the lashes to naturally enhance or add drama. Available in select  stores only.",
   "deep_golden_brown" : "Deep Golden Brown",
   "btm_return_packaging15" : "Empty Foundation Bottle Jar",
   "eye_shadow_veluxe_pearl" : "Vibrantly-toned. Velvety soft. Shimmeringly metallic. Veluxe eye shadow, now overlaid with high-shine pearl. ",
   "alt.h_account_profile.gif" : "Account Profile",
   "submit_es" : "Enviar",
   "country_de" : "Germany",
   "country_rme" : "Middle East",
   "makeup_services_gog" : "In this demonstration, glamour takes centre stage  as a MAC Artist builds on your existing makeup to show the classic and  up-to-the-minute product techniques of today's most dazzling looks.",
   "alt.btn_save_to_fav.gif" : "Save to Favourites",
   "dry" : " Dry",
   "gc_how_delivered" : "How will it be delivered?",
   "alt.show_all_finishes.gif" : "Show all finishes",
   "pnav_disc_prod_copy" : "Your one-stop resource to locate seasonal limited editions or those faves that went farewell. Check its in-stock status. See what our artists recommend in its place.",
   "gc_terms_conditions" : "Terms and Conditions",
   "btm_return_packaging5" : "Empty Paint Tube",
   "waitlist_bloggersobsessions_email_signup_text" : "I’m obsessed! I want to hear from M∙A∙C Cosmetics about future products, services, events and special insider-offers.",
   "btm_return_packaging10" : "Empty Blush compact",
   "alt.pnav_detailed_search_off.gif" : "Detailed Search",
   "to_shop" : "To Shop",
   "alt.send_your_package_off.gif" : "Send Your Package",
   "alt.h_location_time.gif" : "Location & Time",
   "view_all_results" : "View all... Results",
   "n_chars_remaining" : "characthers remaining",
   "alt.gnav_shop_products_off.gif" : "PRODUCTS",
   "alt.btn_add_to_bag_182.gif" : "Add to Bag",
   "please_enter_valid_email_es" : "Ingrese una dirección de email válida",
   "register_pro_read_faq" : "Read about M·A·C Pro Membership.",
   "alt.h_mail_your_packaging.gif" : "Mail Your Packaging and Form to M·A·C",
   "alt.h_my_messages.gif" : "My Messages",
   "country_be" : "Belgium",
   "alt.h_subject.gif" : "subject",
   "powder_blush_sheertone" : "Shy-on tints in a sheer-on micro-refined powder. Not transparent. Goes on faint. Looks real.",
   "buy_now" : "Buy Now",
   "alt.pnav_australia_on.gif" : "Australia (shop online)",
   "lipstick_satin" : "Colour-rich. Soft satin, semi-matte finish. Conditions while adding intense colour.",
   "alt.h_about_mac_gc.gif" : "About M·A·C Gift Cards",
   "alt.h_on_the_art_of_makeup.gif" : "On The Art Of Makeup",
   "alt.i_will_not_attend.gif" : "I will not attend",
   "write_a_review_be_the" : "Be the first to",
   "pro_acct_renewal_payment_methods_title" : "Methods of Payment",
   "select_shade" : "Select Shade by Name",
   "use_mac_products" : "YES, I USE M·A·C PRODUCTS",
   "btm_return_packaging7" : "Empty Mascara Tube",
   "dont_use_mac_products" : "NO, I DON'T USE M·A·C PRODUCTS",
   "black" : "Black",
   "viva_glam_espanol_info" : "Sign me up to be informed with MAC Espanol website is live",
   "member_address" : "Member Address",
   "alt.pnav_email_artist_200x12_off.gif" : "Email an Artist Form",
   "alt.h_my_fav_looks.gif" : "My Favorite Looks",
   "alt.pnav_contact_info_off.gif" : "Contact Information",
   "alt.free_product_off.gif" : "Free Product",
   "lipstick_glaze" : "Low-colour impact with sheen-style finish. Glazes the lips, makes them shine!",
   "pro_acct_sitechange_intl" : "As a registered Pro Member, you will have access to view all M·A·C Products, including Pro Products.  <br><br>To access Pro exclusive content including artistry, trends, Pro events and Pro Membership details, <a href=\"/macpro/\" target=\"_blank\">visit macpro.com</a>.",
   "eye_color" : "Eye Colour",
   "lipstick" : "Lipstick",
   "7_order_status" : "Order Status",
   "email_answer_note" : "Please note that unfortunately the Senior Artists on the M·A·C Pro Team are unable to personally answer questions from the site. Your inquiry will be answered by a M·A·C Artist. All our Online Artists are highly skilled M·A·C trained make-up artists.",
   "alt.signup_image.jpg" : "M·A·C COSMETICS",
   "alt.h_locate_other_events.gif" : "Locate other events",
   "alt.h_date.gif" : "Date",
   "select_topic" : "For answers to common questions we receive, select a topic below.",
   "macpro_service_intro" : "For inquiries about the M·A·C Pro Membership program,   please select a topic below.",
   "mac_security_msg" : "At M·A·C Cosmetics Online we understand how important security is to you. When you place an order at M·A·C Cosmetics Online we encode your information using Secure Socket Layer (SSL) encryption technology. This is the most advanced consumer online security technique to date. You can be assured that your order will be placed safely and securely. We have used government approved encryption software. In order for encryption to take place, your browser must have the SSL protocol. The browsers that have this software are the following:",
   "signed_in_as" : "Signed in as",
   "alt.fnav_viewprofile_off.gif" : "VIEW YOUR PROFILE",
   "lipstick_matte" : "Pigment rich with intense colour pay-off. No-shine, all matte finish.",
   "dropdown_product_no_longer_available" : "Can I obtain a product that is no longer available?",
   "back_to_mac_copy" : "Because we share your commitment to the environment, M·A·C accepts returns of its primary packaging through the Back to M·A·C Program. By returning six [6] M·A·C primary packaging containers to a M·A·C counter or M·A·C Cosmetics online, you’ll receive a free M·A·C lipstick of your choice as our thanks to you.",
   "prolanding_benefits" : "<b>Product Discount</b><br> Present your Membership Card at M·A·C stores and select partnered stores around the world  to   receive a product discount on M·A·C Products, including Pro Products. You may also use your   discount online at www.M·A·Ccosmetics.com. International discounts may vary. Whether you’re on   a shoot or preparing for the stage, M·A·C PRO helps to keep your kit stocked with the    must-have shades, formulas and tools for any industry.<br><br> <b>Pro Only Events</b><br> Master Classes: Integral to the philosophy of the M·A·C culture, inspirational Master Classes   foster the growth of the artist community. Refine and build skills. Meet face-to-face with   celebrated inudstry pro’s. Watch makeup demonstrations to gain insights into application and   technique-theory.<br><br> Pro to Pro: Dialogue with M·A·C PRO Senior Artists in an atmosphere both informative and   informal.  Enjoy cocktails and hors d’oeuvres, mingle with makeup artists, hair stylists, and   connect with agents, models and performers that make your community tick. The ultimate   networking occasion for members to get the buzz and share ideas.<br><br> <b>Industry Privileges</b><br> M·A·C PRO Membership affords you special pro-only privileges. Special offers and discounts on   subscriptions, classes and more.<br><br> <b>M·A·C Pro Online</b><br> Stay informed with everything that’s new and now! Log into M·A·C PRO to enjoy all of your   online benefits including:<br> Pro only event schedules, Trend reports, Member Voice community and networking, Online   Membership Renewal, Artist Tips and Articles, Email An Artist, and the best in Pro Product   information.<br><br> <a href=\"/macpro/\"><font color=\"#FFFFFF\">Visit M·A·C PRO</font></a> to learn more about the benefits of membership.",
   "alt.1_will_attend_with_guest.gif" : "I will attend with a guest",
   "pro_renewal_payment_header" : "Methods of Payment",
   "add_renewal" : "Add renewal to cart",
   "alt.cheeks.gif" : "Cheeks",
   "immed_response_live_chat" : "If you prefer an immediate response and it is between 10:00 am and 10:00 pm EST, Live Chat with a M·A·C Cosmetics Online Makeup Artist.",
   "golden_beige" : "Golden Beige",
   "alt.h_artists.gif" : "ARTISTS",
   "alt.pnav_eurpoe_off.gif" : "EUROPE",
   "hazel" : "Hazel",
   "alt.h_cheek_products.gif" : "Cheek products",
   "search_shades_results" : "Your search returned RESULTS result(s).",
   "special_occasion" : "Special Occasion (prom, wedding)",
   "pro_acct_renewal_no" : "Do not auto-renew my membership",
   "makeup_services_eyes" : "A MAC Artist demonstrates the tools, technicques  and products needed to create the Classic Neutral Eye, a Smoky Eye or  the ultimate fashion-forward Trend Eye",
   "in_us_mail_to" : "In the U.S. Mail to:",
   "eye_shadow_lustre" : "Smoothly pearlized; intensely frosted with deluxe ultra-fine \"conditioned\" finish.",
   "alt.gnav_giving_back_off.gif" : "GIVING BACK",
   "goinggone" : "Your last chance for limited-edition shades and products that are about to be gone.",
   "country_uk" : "United Kingdom",
   "prolanding_membership_program" : "<font color=\"#FFFFFF\">Welcome to M·A·C Pro! Since your last visit, we've made some changes to   the site. Sign in at MAC Pro Online to get Pro-exclusive content. Now shop all products using your Pro discount at maccosmetics.com.</font><br><br>  M·A·C PRO Membership is the industry's leading preferred customer program for the makeup   professional (Makeup Artists, Aestheticians, Cosmetologists, Hairstylists, Fashion Stylists,   Nail Technicians, Costume Designers, Models, On-Air Talent/Performers, and Photographers).   This paid-for membership program offers the makeup professional a direct link to the M·A·C   world as well as member-exclusive benefits. Throughout the program you can count on us to   provide you with the highest level of service, unique educational experiences and the most   comprehensive selection of professional makeup.<br><br> For anyone attending an eligible school or institution of Makeup Artistry or the Performing   Arts, we also offer the M·A·C PRO Student Program. It's the beginning of M·A·C's dynamic   relationship with any makeup professional just starting out. <br><br> <b>How do I become a member?</b><br> For information about program eligibility and details about membership benefits please visit   <a href=\"/macpro/\"><font color=\"#FFFFFF\">M·A·C PRO</font></a> or contact 1-877-553-5536   (available Monday through Friday from 9:00am to 5:00pm) or email <a   href=\"mailto:promembership@maccosmetics.com\"><font color=\"#FFFFFF\">M·A·C PRO   Membership</font></a>.",
   "combination" : " Combination",
   "alt.print_page.gif" : "Print page",
   "country_es" : "Spain",
   "alt.h_mixable_replacement_products_430x13.gif" : "Mixable Replacement Products",
   "prolanding_faq" : "Whether you’re interested in becoming a M·A·C PRO Member or you’re a Member and you still have   a few unanswered questions, here are some of those answers for you. Below are some of the most   frequently asked questions that we receive. Still have more questions? <a   href=\"/customer_service/contact_us.tmpl\"><font color=\"#FFFFFF\">Contact Us</font></a>.<br><br>  <b>Where can I use my discount?</b><br> M·A·C PRO, M·A·C PRO Student and M·A·C PRO Preferred members can redeem their discount in   select partnered stores, at any freestanding M·A·C Store or M·A·C PRO Store around the world.    Discounts may vary from country to country.<br><br>  Members in Canada and the United States can use the discount benefit online at   www.maccosmetics.com, at any M·A·C Store, PRO Store or by telephone through our Customer   Service Department at 800.387.6707, ext. 8555.Unfortunately, at this time members outside of   Canada and the United States are unable to purchase product online at   www.maccosmetics.com.<br><br>  To locate a store location that accepts the M·A·C PRO discount closest to you please <a   href=\"/locator/index.tmpl\"><font color=\"#FFFFFF\">click here</font></a>.<br><br>      If you live outside of the United States and Canada and cannot find a store in your area,   please contact our UK Mail Order Service via phone  01144 870 034 2627 to determine if   delivery options to your area are available.<br><br>  <b>How do I become a member, what discount will I get and am I eligible for the   program?</b><br> For M·A·C Pro Members in the United States or Canada, <a   href=\"/macpro/cms/membership/howtoapply.tmpl\" target=\"_blank\"><font color=\"#FFFFFF\">View   Enrollment for details</font></a>.<br><br>  To apply for the M·A·C PRO Membership outside of Canada and the United States, contact your   local M·A·C Store for further information on the program and eligibility requirements in your   country.  To determine if there is a store near you, <a href=\"/locator/index.tmpl\"><font   color=\"#FFFFFF\">click here</font></a>.<br><br>  For applicants in Australia, France, UK and Spain: applications are available for download,   please <a href=\"/macpro/cms/membership/howtoapply.tmpl\" target=\"_blank\"><font   color=\"#FFFFFF\">click here</font></a>.<br><br>  <b>I want to train as a M·A·C makeup artist. Can you recommend a place where I can train? Is   there a course run by M·A·C?</b><br> At this time, M·A·C does not offer Make Up artistry certification and does not operate a school or   offer courses.  We have the M·A·C PRO Student Program for individuals who attend a school or   institute of learning which has undergone an approval process by our Artist Relations   Department. To see a full list of our eligible schools please <a   href=\"/macpro/cms/membership/prostudent.tmpl\" target=\"_blank\"><font color=\"#FFFFFF\">click   here</font></a>. <br><br>  As a benefit of M·A·C PRO and M·A·C PRO Student Membership we offer Master Classes (generally   2-hour seminars on a specific aspect of Make Up) in M·A·C and M·A·C PRO stores. <br><br>  <b>Does being a part of M·A·C PRO help me qualify for a position to work at a M·A·C store or   counter?</b><br> Thank you for your interest in M·A·C PRO. We are always in search of bright, energetic   individuals to represent us. <br><br>  While M·A·C does offer advanced classes for professional artists in select cities in North   America, we do not have anything of this sort for aspiring artists just starting out.    Membership in M·A·C PRO is not qualification for a position at a M·A·C store or   counter.<br><br>  For a Makeup Artist position within a Department or Specialty store that carries M·A·C PRO,   please inquire with the Human Resources Director of the store and indicate your preference to   work at the M·A·C counter. <br><br>  For a Makeup Artist position in a M·A·C store, please inquire with the Store Manager directly.   To find address and phone number information for the counters in your area, go to <a   href=\"/locator/index.tmpl\"><font color=\"#FFFFFF\">Find Stores</font></a>.<br><br>  <b>I want to sell M·A·C products. What should I do?</b><br> While we appreciate your interest in selling M·A·C products, our products are distributed for   sale only at our authorized retail store accounts, the majority of which are located in major   department stores or international perfumeries. While there are other points of sale, we are   not seeking to expand our distribution network at this time and we do not offer   franchises.<br><br>  <b>Where can I buy PRO product?</b><br> Members in Canada and the United States can use their discount benefit to purchase PRO product   online at www.maccosmetics.com, PRO Store or by telephone through our Customer Service   Department at 800.387.6707, ext. 8555. <br><br>   <b>How long does it take for an application to be processed and how long would it take to be   notified if I was approved or not? </b><br> If you submitted your application in-store please allow for 1-3 weeks before your membership   is processed.  If you sent your application directly to us, please allow 1 week before your   membership is processed.  Upon your application's arrival, your membership will be reviewed   that day.  If your membership is approved, please allow another 1-2 weeks for the delivery of   your card.  If your membership is declined, we will promptly send you a letter with an   explanation.<br><br>  Please remember that your card will be sent to the address that you submitted in your   application.  If you have recently changed address and need a replacement card or wish to   speak with a representative, please contact our processing department directly at 877.553.5536   or 905.940.2437.<br><br>  I am a M·A·C PRO Student member who has graduated. How can I obtain a PRO membership? Congratulations on graduating. If you are now working in the industry, we invite you to join   our M·A·C PRO Membership program and receive even more benefits. Please <a   href=\"/macpro/cms/membership/howtoapply.tmpl\" target=\"_blank\"><font color=\"#FFFFFF\">visit our   How To Apply page</font></a> for details and to obtain an application.<br><br>  <b>How do I apply for the PRO Student Program? </b><br> The M·A·C PRO Student program is open to individuals enrolled in a school or institution of   Makeup Artistry, Cosmetology, Hairstyling, Aesthetics, Photography, Fashion Styling or the   Performing Arts that is on our list of eligible schools. For full details on the Pro Student   Program, Eligible Schools and how to apply, <a href=\"/macpro/cms/membership/prostudent.tmpl\"   target=\"_blank\"><font color=\"#FFFFFF\">click here</font></a>.<br><br>  <b>How do I change my address and contact information? </b><br> Please log in and go to the MY ACCOUNT section, you can change your address, contact   information and email. If you are having difficulty changing your address please email <a   href=\"mailto: macprotech@macpro.com\"><font   color=\"#FFFFFF\">macprotech@macpro.com</font></a>.<br><br>  If you changed your name and require a new card please call our processing department directly   at 877.553.5536 or email your request to <a href=\"mailto:   promembership@maccosmetics.com\"><font   color=\"#FFFFFF\">promembership@maccosmetics.com</font></a>. <br><br>  <b>What do I do if I have lost my card?</b><br> If you changed your name and require a new card please call our processing department directly   at 877.553.5536 or email your request to <a href=\"mailto:   promembership@maccosmetics.com\"><font   color=\"#FFFFFF\">promembership@maccosmetics.com</font></a>.",
   "footer_note_2" : "so we can correct this matter. We apologize for any inconvenience.",
   "not_registered_user" : "NO, I'M NOT A REGISTERED M·A·C USER",
   "step" : "Step",
   "email_confrim_now_that_your" : "Now that you have signed up for email, why not create an account with us? When you have an account with M·A·C Cosmetics Online, you will enjoy many enhanced shopping services.",
   "alt.h_most_important_tool.gif" : "Most Important Tool",
   "alt.h_fav_mac_consumer_prod.gif" : "Favourite M·A·C Product (Consumer)",
   "alt.pnav_canada_on.gif" : "Canada (shop online)",
   "makeup_services_fs" : "A MAC Artist demonstrates how contouring and  high-lighting can create a dimensional and well-defined look by building  on your existing makeup.",
   "lipstick_sheer" : "Colour-light with sheer, near-translucent, naturally-tinted finish.",
   "description_CAT1063" : "In London, anything goes! East London, where I live, is a great melting pot of different people and fashions, and they all influence each other! Club kids, celebrities, media and fashion people, and there’s also the influence of Afro-Caribbean culture. It’s amazing to learn that Girl About Town and St. Germain Lipsticks are wildly popular around here – with me, too! My personal style has always been punk, a softly glamorous punk look…I am forever a fan of Liquidlast Liner in Point Black, Lip Pencil in Cherry, Ruby Woo Lipstick and Red Lipmix. The starting point for any beautiful makeup is gorgeous skin that looks naturally perfect. My faves are Face and Body Foundation, Select Moisturecover and Studio Finish Skin Corrector. And Lashes make eyes look bigger and more powerful. The bestseller worldwide is 7 Lash so you know they’re a sure winner, but 4 Lash is my favourite because they look fluffy and real!",
   "alt.return_packaging_off.gif" : "Return Packaging",
   "enter_location_event_search" : "TBD",
   "register_country_error" : "Please enter your country matching your membership information.",
   "found_locations" : "Found Locations",
   "alt.btn_add_to_bag_93.gif" : "Add to Bag",
   "btm_return_packaging1" : "Empty Eye Shadow Pan",
   "blushcreme_pearl" : "Soft and creamy, with a sophisticated pearl finish. Brings a vibrant, lightly reflective shimmer to the cheeks. Blends well, layers to the desired finish.",
   "no_search_word" : "Please enter a keyword to search.",
   "footer_copyright" : "© Make-up Art Cosmetics. All Worldwide Rights Reserved.",
   "alt.fnav_phone_num.gif" : "1-800-588-0070",
   "alt.h_country_chooser.gif" : "Country Chooser",
   "out_of" : "out of",
   "unsubscribe_contact_cs" : "Contact a customer service representative.",
   "alt.h_discontimued_prods_250.gif" : "Discontinued Products",
   "alt.lips.gif" : "Lips",
   "1_find_products" : "Find MAC Products",
   "contactus_email_subject_usca" : "Thank you for your email",
   "alt.h_events_near_you.gif" : "Events Near you",
   "alt.h_most_respected_designers.gif" : "Most Respected Designers",
   "pro_acct_renewal_note" : "PLEASE NOTE: You must sign up for auto-renewal prior   to the month in which your membership expires.",
   "alt.pnav_me_on.gif" : "Middle East",
   "shade_product_name" : "Shade/Product Name",
   "unsubscribe_valid_member" : "As a valued member of the M·A·C community, your privacy is important to us. We will never provide your personally identifiable information to third parties for their use in marketing their products or services to you without your consent. Review our privacy policy.",
   "eye_shadow_frost" : "An iridescent shine that adds a highlight to any colour. ",
   "pro_renewal_intro" : "Choose to renew your membership before it expires. You will continue through our easy checkout process. Once your payment is received, a new card will be sent to the address saved in your Member Profile. If your membership expires, you may only renew via phone or mail.* Your M·A·C PRO Membership lasts for one year.",
   "dropdown_product_concerns" : "Concerns about a M·A·C Product (address required)",
   "private_function_note" : "If you are interested in having a M·A·C Artist attend a private function, please contact a M·A·C store in your area.",
   "country_sg" : "Singapore",
   "6_disc_products" : "Discontinued Products",
   "alt.btn_email_directions.gif" : "Email Directions",
   "alt.h_my_favorites" : "My Favorites",
   "alt.print_your_form.gif" : "print your form",
   "comment" : "My Comments",
   "goodbyes" : "Goodbyes is the perfect last-chance resource to find end-of-line shades and products. Here is a tool to source your most-wanted:",
   "signin_pro_clicktoregister" : "click here to register.",
   "makeup_services_jol" : "A personalized product demonstration focusing on  one key feature of your face",
   "read_more" : "Read More",
   "results" : "results",
   "thanks_es" : "¡Gracias!",
   "shoptogether_landingpage_meta_description" : "Join me for some virtual shopping. M·A·C Shop Together is just like being in a store together. We can instant message & see what we’re browsing in real time. Ready? Let’s go!",
   "radiant_or_dewey_finish" : "Radiant or Dewy Finish",
   "normal" : " Normal",
   "jump_to_date" : "Jump to Specific Date",
   "country_hk" : "Hong Kong",
   "waitlist_bloggersobsessions_info" : "Thank you so much for all the <3! All nine of our superstar Beauty Bloggers’ shades of Eye Shadow and Lipglass SOLD OUT fast. Sign up NOW to receive an alert when these shades are available again while supplies last.",
   "alt.h_start.gif" : "Start",
   "alt.sales_tax.gif" : "Sales Tax",
   "btm_return_packaging14" : "Empty Foundation Tube",
   "btm_return_packaging6" : "Empty Pigment",
   "alt.pnav_mac_aids_fund_off.gif" : "MACAIDSFUND.ORG",
   "email_signup_language_preference_header" : "Receive M·A·C emails in:",
   "alt.pnav_terms_conditions_off.gif" : "Terms & Conditions",
   "gc_returns_exchanges" : "Returns and Exchanges",
   "alt.pnav_my_favorites_off.gif" : "My Favorites",
   "country_ca" : "Canada",
   "email_intro" : "Please use this form below to get in touch with us. Due to the volume of email we receive, it may take us a few days to respond. Please be sure to give us your full email address so we may respond to your inquiry. Between the hours of 10:00 am and 10:00 pm EST, Live Chat with a M·A·C Makeup Artist Online. We do appreciate the fact you've taken the time to let us know what you think, and we will do our best to incorporate appropriate suggestions into the site.",
   "alt.btn_back_to_trends.gif" : "Back to Trends",
   "alt.btn_play_colour.gif" : "Colour Play",
   "btm_return_packaging8" : "Empty Liquid Liner Tube",
   "alt.h_fav_tool_technique.gif" : "Favourite Technique",
   "btm_street" : "7603 Trade Port Drive",
   "alt.h_artist_tips.gif" : "Artist Tips",
   "search_shades_no_results" : "Your search returned 0 results.",
   "register_zip_error" : "Please enter the zip/postal code matching your membership information.",
   "makeup_services_learn" : "See the difference MAC can make as our Artists  reveal their tips, secrets and techniques. In-store product  demonstrations are available on a walk-in basis any day. What you take  away is always what you learn - our Pro know-how. Complimentary. Based  on Artist availability.",
   "alt.pnav_product_info_off.gif" : "Product Information",
   "waitlist_bloggersobsessions_thanks_header" : "/images/popup/bloggers/thankyou.jpg",
   "alt.pnav_back_To_mac_off.gif" : "BACK TO M·A·C",
   "country_fr" : "France",
   "packaging_1" : "Packaging No.1",
   "limited" : "Limited Edition",
   "first_choice" : "First Choice",
   "alt.signup_header.gif" : "Thank you for signing up for email from M·A·C COSMETICS",
   "preferred_look" : "Preferred Look",
   "alt.h_questions_concerns.gif" : "Questions or Concerns",
   "makeup_services_unavail" : "Alabama, Minnesota, DC, Idaho, Oklahoma,  Maine, Montana.",
   "alt.h_most_afmired_artists.gif" : "Most Admired Makeup Artists",
   "country_ru" : "Russia",
   "meta_desc_homepage" : "Professional makeup artist quality cosmetics. Offering more than 100 shades for eyes, lips and face…everything a makeup addict can’t live without.",
   "pro_acct_renewal_yes" : "Set my membership to automatically renew each year.",
   "country_kr" : "Korea",
   "pro_acct_renewal_payment_methods" : "Credit Cards (excluding Discover Card) Accepted<br>$35 US/$45 CDN Annual Membership (non-refundable)",
   "email_confirm_subject" : "MAC Cosmetics Online Email Signup Confirmation",
   "country_au" : "Australia",
   "gc_where_purchased" : "Where can it be purchased?",
   "alt.btn_return_to_map.gif" : "Return to map",
   "email_confrim_regards" : "Best Regards",
   "alt.alternate_image.gif" : "Alternate Image",
   "dropdown_order" : "My M·A·C Order",
   "contactus_email_subject_pro" : "Thank you for your comments",
   "pro_acct_renewal_cc_expiration_following" : "Your information indicates that your credit card will expire at the end of this month. In order to successfully process your auto-renewal payment, please update your credit card information.",
   "eye_shadow_matte2" : "Intense, opaque matte finish with exceptionally rich colour payoff.",
   "btm_return_packaging2" : "Empty Trio Compact",
   "viva_glam_espanol_info_es" : "¡Inscríbete y te informaremos cuando salga el sitio web de M·A·C en español!",
   "sign_up_for_email" : "Email Me",
   "tracking_note" : "(multiple tracking numbers per order indicate separate shipping",
   "search_shades" : "Search shades",
   "events" : "events",
   "description_cat1055" : "Tokyo is the city where everything is stuffed to the max, styles clash and most importantly, it’s a place where you're completely free to display your own individuality. “Kawaii” makeup is hot right now. More natural than it has been last season. Sheertone Blush <span class=\"CAT1055PROD329SKU2967 shoppable\">Peaches</span> and <span class=\"CAT1055PROD329SKU2965 shoppable\">Pink Swoon</span> are IT! Dazzleglass <span class=\"CAT1055PROD1765SKU10784 shoppable\">Smile</span> is the must-have product for the new shiny looks. <span class=\"CAT1055PROD363SKU1662 shoppable\">Orb</span> Eye Shadow is particularly necessary for Asian models. Suddenly, instant brightness! And I can't leave this out: Fluidline is almighty. It can be used as eyebrow enhancer, eyeliner and eye shadow! That’s pretty incredible. The MUST to be added this year to every stylish Tokyo girl’s kit is <span class=\"CAT1055PROD970SKU4164 shoppable\">Prep + Prime Lash</span>.  Actually, it’s for eyelashes, but use it for eyebrows! All tidy in seconds! To go from day to night – and nightlife is quite spectacular and sometimes carnivalesque in Tokyo – add <span class=\"CAT1055PROD505SKU16378 shoppable\">Reflects Pearl</span> Glitter. Believe me, it gets everyone’s attention. And of course it’s mandatory to start with the perfect blank canvas: Studio Sculpt SPF 15 Foundation. Models very often have troubled skin. It’s saved me over and over!",
   "email_address_es" : "Dirección de email",
   "isp_msg_pro" : "PLEASE NOTE: Your Internet Service Provider (ISP) or email client may be using filters to block email that may affect the receipt of email from maccosmetics.com. To be sure that our response reaches you, please consult the help section of your email program or contact your ISP directly to see how to relax or remove the settings that filter or block email. This will ensure that correspondence from maccosmetics.com will be accepted. Additional Information is available in our FAQ section above. If you have further questions, please contact us at 1-800-866-6464.",
   "call_to_place_order" : "1. Call 1-800-588-0070 to place your order.",
   "medium_to_dark" : "Medium to Dark",
   "deep_brown_pink" : "Deep Brown Pink",
   "register_pro_intro" : "If you already have a M·A·C Pro member number, you can use the form below to complete your online registration.",
   "alt.btn_submit.gif" : "Submit",
   "next" : "Next",
   "pro_renewal_footnote" : "*Please note: If your membership has lapsed for twelve months or more, you must resubmit current professional documentation with your M·A·C PRO Membership application by mail.",
   "footer_note_1" : "Please do not reply to this message. If you have received this message  in error, please forward a copy of the entire message, including the headers to",
   "back_to_mac_address" : "Back to M·A·C",
   "brand_member" : "Are you a brand member?",
   "natural" : "Natural",
   "unsubscribe_communication" : "Communication from M·A·C Cosmetics Online keeps you tuned in to all things M·A·C. You hear from us about products, services, events and special insider only offers.",
   "show_limited_edition" : "Show only Limited Edition",
   "alt.terms_conditions.gif" : "Terms and Conditions",
   "don’t_know" : " Don’t Know",
   "alt.pnav_consumer_awareness_off.gif" : "Consumer Awareness",
   "packaging_5" : "Packaging No.5",
   "email_confrim_view" : "View your Order Status and track your shipments.",
   "phone_number" : "Phone Number",
   "alt.btn_chat_live.gif" : "Chat Live with our Makeup Artists - Chat Live",
   "signin_pro_registration" : "If you are a M·A·C PRO member who has previously registered on our site, please sign in under \"Already Registered\" with your email and password. If you are a member and would like to register to receive your special PRO privileges,",
   "pro_no_renewal_history" : "You have no past renewal charges on record.",
   "alt.returns_exchanges.gif" : "Returns and Exchanges",
   "brand_user" : "Are you a brand user?",
   "gc_redeeming_gc_online" : "Redeeming M·A·C Gift Cards at M·A·C Online",
   "11_order_cancellation" : "Order Cancellation",
   "2_careers_corporate" : "Careers with MAC Corporate",
   "grey" : "Grey",
   "will_do_best_to_fulfill_note" : "We will do our best to fulfill your preference in the order you have stated based on inventory availability",
   "pink" : "Pink",
   "powder_blush_sheertone_shimmer" : "Jet-milled and micro-fine, just like Sheertone, but tickled with just-enough shimmer to make light dance on the cheeks.",
   "dropdown_product_alt" : "Can you recommend a alternative for a discontinued product or shade?",
   "pro_discount" : "",
   "pro_acct_renewal_payment" : "When enrolled in this program please make sure that   your payment information is up to date.",
   "use_form" : "Please use this form below. Due to the volume of email we receive, it may take us a few days to respond.",
   "alt.h_prev_purchased.gif" : "Previously Purchased",
   "undertone_of_skin" : "Undertone of Skin",
   "makeup_services_fof" : "With a shade palette customized to your skin tone,  this demonstration builds on your existing makeup to show you how to use  product to enhance features.",
   "email_thanks" : "Thank you, your questions have been submitted.",
   "select" : "Select",
   "email_confrim_review" : "Review and shop from your Order History.",
   "alt.h_my_fav_tips.gif" : "My Favorite Tips",
   "alt.h_select_return_packaging" : "Select Your Return Packaging",
   "country_us" : "United States",
   "write_a_review" : "Write a review",
   "eye_shadow_matte" : "Hi-colour pay-off in a no-shine matte finish. ",
   "items_in_favourites" : "items in favourites",
   "alt.btn_compare_brushes.gif" : "Brush Finder",
   "btm_city_state_zip" : "Louisville, KY 40258",
   "btm_return_packaging13" : "Empty Foundation Bottle",
   "chrs_remaining_plural" : "characters remaining",
   "country_in" : "India",
   "mac_me_over_campaign" : "Do you have style, heart and soul? Compete for a chance to become the face of M·A·C’s Fall 2011 campaign at <a href=\"http://www.macmeover.com\" target=\"_blank\">www.macmeover.com.</a>",
   "alt.eyes.gif" : "Eyes",
   "alt.h_new.gif" : "New",
   "register_pro_last_name" : "Your last name as it appears on member card",
   "alt.btn_print_look.gif" : "Print look Information",
   "alt.btn_get_directions.gif" : "Get Directions",
   "oily" : " Oily",
   "alt.fnav_choose_country_off.gif" : "Choose your Country",
   "country_nl" : "Netherlands",
   "alt.h_rsvp_event.gif" : "RSVP Event",
   "most_primary_packaging" : "* Most primary packaging of M·A·C products is accepted as Back to M·A·C \"currency,\" M·A·C does not accept the following as a part of the program:  secondary packaging of any sort (paper box, plastic/cellophane wrapper, shopping bags, etc), samples (sample containers, special/trial size products for promotional/seasonal kits), cosmetic applicators/tools",
   "green" : "Green",
   "artists_replacements" : "Our artists have recommended the following replacements for you.",
   "alt.h_lip_products.gif" : "Lip products",
   "alt.h_featured_products.gif" : "Featured Products",
   "nicki_minaj_info" : "Check it Out: Nicki Minaj's exclusive lipstick Pink Friday is only available for purchase Fridays! Sign up here to receive an alert when it is available while supplies last.",
   "gc_where_redeemed" : "Where can it be redeemed?",
   "alt.h_replacement_product_430x13.gif" : "Replacement Product",
   "brown" : "Brown",
   "please_describe_concern" : "Please describe your question or concern in as much detail as possible.",
   "alt.pnav_my_account_off.gif" : "My Account",
   "your_personal_message" : "Your personal message will be placed here.",
   "alt.fnav_sitemap_off.gif" : "Site Map",
   "register_birth_error" : "Please enter your birth month and day matching your membership information.",
   "membership_id" : "Membership ID:",
   "eye_shadow_velvet" : "Soft look finish with high-colour intensity. Has a plush velvety look and feel.",
   "artists_intro" : "They're our Artists backstage, on set, behind the scenes. Our all-star pros. Find out who they are. Where they're coming from. Latch onto their secrets. Lash onto a look. Brush up on techniques. <a href=\"http://www.macartiststweets.com/\" target=\"new\">Follow M·A·C Artists Tweets</a>.",
   "not_registered_user_pro" : "NO, I'M NOT A REGISTERED M•A•C PRO USER",
   "third_choice" : "Third Choice",
   "waitlist_email_required" : "Please enter your email address",
   "signup_thanks" : "Your address was added to our mailing list.",
   "alt.h_artists_tools_430.gif" : "Artist’s Tools",
   "alt.pnav_employment_off.gif" : "Employment",
   "mac_me_over" : "Enter M·A·C Me Over!",
   "eye_shadow_satin" : "Pure colour in a satin-style finish. Provides a soft subtle non-frost, light-refractive sheen.",
   "btm_return_packaging3" : "Empty Quad Compact",
   "contactus_email_subject_artist" : "Ask an Artist",
   "find_store" : "Find a store or event",
   "print_form_complete" : "your form is now complete. Please ensure to print your form and mail it to the address below.",
   "alt.how_delivered.gif" : "How will it be delivered?",
   "skin_type" : "Skin Type",
   "please_enter_valid_email" : "Please enter a valid email address",
   "packaging_2" : "Packaging No.2",
   "alt.collection_products.gif" : "Collection Products",
   "8_shipping_info" : "Shipping Information",
   "country_at" : "Austria",
   "gc_select_topic" : "For more information about M·A·C Gift Cards, select a topic from below.",
   "4_appointments" : "Bridal or Private Appointments",
   "country_cn" : "China",
   "description_CAT1093" : "Don’t neglect your skin! It all starts with three products I can’t live without. First, cleanse delicately with Lightful Foaming Crème Cleanser then treat with Care Blend Essential Oils. They leave the skin feeling incredibly supple without any tackiness. And my best secret for gorgeous skin: mix Lightful Charged Essence into your foundation for a mannequin-like finish. Another product I love is Fix+. It does everything! Blends and sheers an “over-concealed” eye, highlights skin, creates a rich finish when mixed with mineral eye shadows and blushes and sets makeup when you’re done! Of course, I’m all for colour, too. I love subversive, unexpected placement of shadows, like electric blues in the inner and outer corner of the eye and acid green in the middle, all applied organically, and topped with gloss for a watercolour feel! If you want to know my favourite product to give away (and sometimes it grows legs of its own), it’s Plushblack! Instant drama!",
   "gc_lost_stolen_gc" : "Lost or Stolen M·A·C Gift Cards",
   "alt.h_artistry_in_action_430.gif" : "Artistry in Action",
   "9_apo_fpo" : "APO / FPO, P.O. Boxes and International",
   "zip_code_colon" : "zip code:",
   "alt.h_email_an_artist.gif" : "EMAIL AN ARTIST",
   "alt.gnav_artistry_off.gif" : "ARTISTRY",
   "callout_viva_glam" : "Sign up to spread the word &amp; raise awareness about the impact of HIV/AIDS on women, men and children everywhere. Visit <a href=\"http://www.vivaglam.com\">www.vivaglam.com </a>, see Gaga’s VIVA GLAM video by Nick Knight &amp; join the conversation via Tweet #macvivaglam.",
   "alt.btn_find_events.gif" : "Find Events",
   "medium" : "Medium",
   "email_signup_text_es" : "Deseo inscribirme y recibir información de M·A·C Cosmetics sobre futuros productos, servicios, eventos y ofertas especiales sólo para miembros.",
   "alt.gnav_my_mac_off.gif" : "My M·A·C",
   "description_cat1090" : "I live in Orange County and have all of my life. I won’t be caught out and about in the O.C. without Studio Fix Powder Plus Foundation, Fix + , Studio Finish Concealer SPF 15 and <span class=\"CAT1090PROD305SKU2960 shoppable\">Show-off</span> Brow Set (I use it on my eyebrows AND beard)! I also want a totally tan look for the summer months so I use <span class=\"CAT1090PROD302SKU3989 shoppable\">Matte Bronze</span> Bronzing Powder with my trusty <span class=\"CAT1090PROD304SKU5401 shoppable\">129 Powder/Blush Brush</span>. I do enjoy the beach like most Cali natives, so I always protect my face with <span class=\"CAT1090PROD1544SKU4657 shoppable\">Prep + Prime Face Protect SPF 50</span>. It stays on ridiculously better than regular sun block! I live for our <span class=\"CAT1090PROD472SKU4920 shoppable\">Lip Conditioner</span> (the one that comes in a squeeze tube). My lips are NEVER dry. I can even skip a day! There is a huge scene in Orange County that is inspired by the 1950s-1960s. Flawless put-together makeup. Studio Stick Foundation SPF 15 with Mineralize Foundation/Loose SPF 15 on top. For eyes, they want a sharp, black Marilyn Monroe line and they’re crazy for new <span class=\"CAT1090PROD4253SKU10457 shoppable\">Penultimate Eye Liner</span>. These ladies look like Golden-Era Hollywood Starlets. I don’t think this look is going anywhere – a certain blonde from the O.C. who sings for a super-popular band put the look on the map globally and we are all very proud…I am at least! There are plenty of girls who keep it simple, fresh and what I like to call “Beach Beauty”. These girls love Face and Body Foundation or Studio Moisture Tint SPF 15. They prefer to intensify natural features so I always use <span class=\"CAT1090PROD7207SKU16434 shoppable\">Opulash Mascara</span> – my all-time favourite mascara! On the lips it’s usually a warmer nude shade like <span class=\"CAT1090PROD310SKU10675 shoppable\">Shy Girl</span>, <span class=\"CAT1090PROD310SKU2773 shoppable\">Hug Me</span>, <span class=\"CAT1090PROD310SKU3039 shoppable\">Cherish</span> or <span class=\"CAT1090PROD310SKU2788 shoppable\">Jubilee</span> Lipstick with <span class=\"CAT1090PROD309SKU2691 shoppable\">Viva Glam V</span> Lipglass – something every O.C. fashionista has.",
   "lipstick_frost" : "Excellent colour payoff with medium to high frosted shimmer-and-shine finish.",
   "country_jp" : "Japan",
   "alt.pnav_sth_america_off.gif" : "SOUTH AMERICA",
   "no_pro_membership_payments" : "No pro payments to date.",
   "gc_sales_tax" : "Sales Tax",
   "dropdown_find_product" : "Where can I find M·A·C products near me (address required)",
   "waitlist_bloggersobsessions_thanks_body" : "Thank you! We will alert you when the second Bloggers’ Obsessions are available again!",
   "email_confirm_success" : "You have successfully signed up for email from M·A·C Cosmetics Online. In the coming weeks, you will begin hearing about products, services, events and special insider only offers. We appreciate your interest in M·A·C Cosmetics and look forward to keeping you tuned in.",
   "alt.fnav_customer_service_off.gif" : "Customer Service",
   "alt.pnav_shopping_offline_off.gif" : "Shopping Online",
   "select_finish" : "Select a finish",
   "download_flash" : "Download Flash",
   "email_confrim_qualify" : "Qualify to redeem special offers such as Free Shipping.",
   "directions" : "Drag swatch off palette to remove",
   "powder_blush_satin" : "Light refractive sheen-style finish. Adds subtle highlights to the skin.",
   "country_chooser" : "Country Chooser",
   "gc_maximum_purchase_policy" : "Maximum purchase policy",
   "alt.limited_edition_packaging.gif" : "Limited Edition Packaging No Longer Available",
   "favourites" : "favourites",
   "found_events" : "Found Events",
   "alt.pnav_mac_stores_off.gif" : "M·A·C Stores",
   "pro_existing_acct_pw" : "Do you already have an existing maccosmetics.com account? If so, please enter the email address and password of that account below and we will merge your account information into your new M·A·C Pro account.",
   "yellow_golden_olive" : "Yellow-Golden Olive",
   "tartan_special_message" : "Tartan Tale Eye Shadows are now 25% off. Your M·A·C Pro discount does not apply online at this time.",
   "alt.title_overallrating-blk.gif" : "OVERALL RATING",
   "white" : "White",
   "very_fair" : "Very Fair",
   "alt.btn_print_directions.gif" : "Print Directions",
   "see_all_product_shades" : "See all ::product name:: shades",
   "eye_shadow_veluxe" : "Superfine pigment rich and very luxe. Supple to apply. Provides silky smooth matte finish. ",
   "alt.remove_off.gif" : "Remove",
   "packaging_6" : "Packaging No.6",
   "alt.h_attributes.gif" : "Attributes",
   "btm_return_packaging9" : "Empty Powder Compact",
   "gnav_checkout" : "Checkout",
   "unsubscribe_subject" : "unsubscribe subject line",
   "btm_name" : "Back to M&middot;A&middot;C",
   "skin_tone" : "Skin Tone",
   "gc_delivery_cost" : "How much does delivery cost?",
   "alt.btn_view_look.gif" : "VIEW LOOK",
   "dropdown_artist" : "Ask a M·A·C Makeup Artist",
   "alt.btn_add_look_to_bag.gif" : "Add look to bag",
   "second_choice" : "Second Choice",
   "makeup_services_intnote" : "Also pioloting internationally in Germany,  South Africa, Sweden, Denmark, Russia, Greece, (TBC), Hong Kong,  Thailand, Australia, Japan, Malyasia, New Zealand, Taiwan, China,  Indonesia, Philippines, Singapore, LATAM: Argentina, Brazil, Mexico,  Peru, Venezuela, Chile",
   "invalid_email" : "The email address entered is invalid.",
   "alt.collection_looks.gif" : "Collection Looks",
   "beige" : "Beige",
   "pro_renewal_payment" : "Credit Cards (excluding Discover Card) Accepted$35 US/$45 CDN Annual Membership (non-refundable)",
   "alt.i_will_attend.gif" : "I will attend by myself",
   "alt.h_yourcontactinfo.gif" : "Contact Information",
   "alt.pnav_search_discont_products_off.gif" : "Search Discontinued Products",
   "detailed_search" : "Detailed Search",
   "alt.pnav_privacy_policy_off.gif" : "Privacy Policy",
   "blue" : "Blue",
   "alt.h_fav_feature.gif" : "Favourite Feature",
   "pro_acct_renewal_reference" : "Reference #",
   "alt.pnav_ordering_offline_off.gif" : "Ordering Online",
   "lipstick_lip_treatment" : "Colour-free. Texture-free. Provides natural healthy-looking lips.",
   "goodbyes_spp" : "Your last chance to find that seasonal trend shade, fabulous limited-edition, or favourite products that are about to go on a permanent vacation. Quick, get them before they're gone!",
   "alt.h_contact_info.gif" : "Contact Information",
   "alt.fnav_terms_off.gif" : "Terms/Conditions",
   "alt.pnav_my_profile_off.gif" : "My Profile",
   "light_brown" : "Light Brown",
   "alt.free_shipping.gif" : "Free Shipping",
   "email_confrim_mac_acct_mgmt" : "M·A·C Cosmetics Online Account Management http://www.maccosmetics.com",
   "in_ca_mail_to" : "In Canada Mail to:",
   "btm_return_packaging12" : "Empty Lipglass Tube",
   "description_cat1089" : "New York is my town but I live in Brooklyn – the melting pot of bad and good makeup trends – past, present and future! In Manhattan loud lip colour is back! But I love the girls from eras past in Brooklyn who were trendy then and stayed there! They remind me of where makeup has been and what could be fabulously reinvented! Here, no matter who she is and what she likes – she is wearing <span class=\"CAT1089PROD7207SKU16434 shoppable\">Opulash</span> and <span class=\"CAT1089PROD4462SKU10795 shoppable\">Prep + Prime Transparent Finishing Powder</span>. I love <span class=\"CAT1089PROD305SKU2963 shoppable\">Girl Boy</span> BrowSet to add subtle highlights and <span class=\"CAT1089PROD263SKU1296 shoppable\">Stud</span> Brow Pencil to sketch in razor sharp volume. The four musts in my ultimate kit would be Fix + (to sheer out and blend things), Select Cover-up Concealer (because it is a durable matte finish), <span class=\"CAT1089PROD4467SKU10772 shoppable\">Skin Refined Zone Treatment</span> (I use this as a liquid powder – and it makes you look younger and tighter!) and Lipmix (it is so vibrant and the texture is just amazing!). Fall is eyeliner time. But be warned – Feline Kohl Power sells out fast! It gets \"permanently borrowed\" backstage all the time!! If black isn't your thing – <span class=\"CAT1089PROD323SKU1451 shoppable\">Powersurge</span> makes you look candlelit and soft.",
   "maximum_215_chars" : "(maximum 215 characters)",
   "contact_us_thank_you" : "We value your interest and look forward to serving you. If your question or concern is urgent, please call us at 1-800-588-0070 and we will be happy to assist you. <br> PLEASE NOTE: Your Internet Service Provider (ISP) or email client may be using filters to block email that may affect the receipt of email from maccosmetics.com. To be sure that our response reaches you, please consult the help section of your email program or contact your ISP directly to see how to relax or remove the settings that filter or block email. This will ensure that correspondence from maccosmetics.com will be accepted. Additional Information is available in our FAQ section. If you have further questions, please contact us at 1-800-588-0070."
};
        
            rb.error_messages = {
   "required.state" : "Please select the state for this address. ",
   "invalid.password.registration" : "Please enter a valid password with 6-12 alphanumeric characters.",
   "delivery" : "",
   "unicode_script.first_name_alternate" : "Please enter First Name with katakana.",
   "session_timeout" : "You have been signed out due to inactivity. Please sign in again. ",
   "taken.mobile_email_address" : "The mobile email address you entered is already registered.",
   "sms.subscribe.duplicate_reference" : "You have already subscribed for SMS updates with a different account.",
   "required.ship_zip" : "Please enter a postal code.",
   "required.country_id" : "Please select a Country.",
   "signin_error2" : "We do not recognize your sign in information. Please try again. Please note the password field is case sensitive.",
   "signin_replen" : "You have selected auto replenishment. This option requires a [BRAND].com account. It appears that you already have an account with us, <a href=\"/templates/session/checkout_signin.tmpl\">please sign in now.</a>",
   "required.phone" : "Please enter your phone number.",
   "required.pc_eyecolor" : "Please tell us the color of your eyes.",
   "offer_criteria_not_met" : "You did not meet the criteria for offer code ::offer_code::.",
   "unavailable.payment_type.cod.no_cod_to_postal_code" : "We're sorry, but COD is not available at this address.",
   "required.ship_city" : "Please enter the city/town for this address.",
   "no_to_address" : " No to address specified for emailer.",
   "format.password" : "Please enter 6-12 alpha-numeric characters.",
   "required_or.email_address.device_id" : "Please enter your email.",
   "required.pp_payment_type" : "Please, choose the payment method below.",
   "required.payment_type" : "Please select a payment method.",
   "error_find_page" : "To see if the page is still available or to find similar information, please try the following:",
   "requiredand.sms_promotions.phone2.carrier_code.email_signup" : "Please review your information below and add any missing information.",
   "cart.cost_limit" : "There is a maximum of US $500.00 per order. Please remove some items from your cart. For details, refer to our <a  href=\"/cms/customer_service/shoponline.tmpl#max_purchase\"><font color=\"#333333\">Maximum Purchase Policy</font></a>.",
   "email_signup_deja_vu" : "To change your email address or unsubscribe to the email newsletter please visit the update page. ",
   "required_dependency.sms_promotions.phone2" : "Please enter your mobile number.",
   "required.pc_sunresponse" : "Please tell us how your skin responds to the sun.",
   "no_addr_found" : "That address was not found in your address book. ",
   "other_payment_method" : "OTHER PAYMENT METHODS",
   "required_or.phone1.phone2" : "Please enter a phone number.",
   "account_change_not_requested" : "IMPORTANT: If you did not request this change, or if this request appears to have been made by an unauthorized person, please call Customer Service at 1-800-BRAND (::variable::).",
   "addtocart_fss_met" : "You have qualified for free shipping!",
   "addtocart_fss_unmet" : "Only ::needs:: away from free shipping!",
   "required.email_address_verify.waitlist_email_signup" : "Please re-enter your email address to verify it.",
   "required.question.macpro_contact_us" : "Please select Inquiry.",
   "obscene.custom_username.macpro_member_registration" : "Bad language filter. Please enter a new screenname.",
   "line_item_added" : " ::PRODUCTNAME:: in ::PROPERTY:: has been added to your shopping bag.",
   "sms.carrier.update" : "Your mobile carrier has been updated.",
   "session_pw_hint" : "Please enter your email address to receive your password hint.",
   "required.comments.macpro_artist_email" : "Please add a Comment for the MAC PRO Artist.",
   "ccnumber" : "Please supply your credit card number.",
   "required.retpack5.giving_back" : "Please select your fifth return product.",
   "format.card_message.checkout_giftmessage" : "We're sorry. A character you have entered is not able to be accepted. Please use only English or Spanish characters for gift messages.",
   "error_page_moved" : "The page you are looking for is no longer available or has been moved.",
   "invalid.pc_email_address" : "Please enter your email address in the following format: jane@aol.com",
   "required.email_address_verify.email_signup" : "Please reenter your email address for verification.",
   "no_account" : "We do not recognize your sign in information. Please try again. Please note the password field is case sensitive.",
   "account_prod_sold_out" : "This product is sold out and is no longer available online.",
   "cc_deleted" : "Your credit card has been deleted.",
   "comments" : "A question choice is required.",
   "required_dependency.carrier_code.phone2" : "Please enter your mobile number.",
   "required.user_choice" : "The Contest Entry Field is required.",
   "update_acct" : "Your account information is not yet complete. Please update below.",
   "required.retpack6.giving_back" : "Please select your sixth return product.",
   "offer_deferred" : "Thank you. We will save the offer code you entered and apply it before you submit your order. Please look for details on the Order Review page.",
   "required.zip" : "Please enter your zip code.",
   "restricted.ship_method.pobox" : "Second Day and Overnight Service are not available for orders being sent to APO/FPO addresses or P.O. Box addresses.",
   "msg_hazmat.pobox" : "Please note: Due to the contents of your order, one or more items will be shipped separately and may take up to 10-14 business days to be delivered. Please expect more than one delivery for this order.",
   "cart.cost_limit_with_exceptions" : "Your order cannot exceed a total of US $500.00 per order. You are also able to purchase 1 Basic Colour Kit or 1 Advanced Colour Kit or 1 Foundation Kit. Please remove some items from your cart. For details, refer to our <a  href=\"/cms/customer_service/shoponline.tmpl#max_purchase\"><font color=\"#333333\">Maximum Purchase Policy</font></a>.",
   "required.ship_address" : "Please enter your address.",
   "required.offer_products_flag" : "The offer code ::variable:: is not valid.",
   "required_and.phone1_area_code.phone1_part1.phone1_part2" : "Please provide your cell phone number.",
   "identical.email_address.email_address_verify.identical.email_signup" : "Please check that the email address in the two fields are identical.",
   "spp_long_out_of_stock" : "This item is currently not available. You will be kept informed via email.",
   "not_registration_offer" : "Sorry, offer code ::OFFER_CODE:: is not for use during registration.",
   "multiple_offer_codes" : "We can only accept two (2) offer codes per order.",
   "account_email_error" : "Please check that the email address in the two fields are identical.",
   "unicode_script.last_name_alternate" : "Please enter Last Name with katakana.",
   "personal_information" : "Personal Information",
   "error_page_not_found" : "Sorry, we can't find that page.",
   "reset_password_current_password_mismatch" : "The password supplied did not match the one in our records.",
   "acct_notfound" : "Please review your information and try logging in again. If you continue to experience difficulty logging into the site, please contact us at <a href=\"mailto:promembership@maccosmetics.com\"><font color=\"#333333\">promembership@maccosmetics.com</font></a>.",
   "nullpw" : " Tried to set a null password cookie!",
   "signin_optin" : " Our records indicate you have already registered on [BRAND] Online.  Please sign in to opt-in for emails.",
   "survey_over" : "Unfortunately, the survey has ended.",
   "pro_renewal_payment.required" : "Please provide your default payment information to activate Pro auto renewal.",
   "offer_registered_only" : "Sorry, offer code ::OFFER_CODE:: is only available for registered customers.",
   "fss_nonstandard_method" : "This offer code is for Free Standard Shipping.  Please re-enter your offer code and select Standard Service.",
   "cardnumber_type_mismatch" : "The Payment Type does not match your Credit Card Number. Please check your Payment Type.",
   "required.question.contact_us" : "Please select Inquiry.",
   "user.query_by_email_address.no_account" : "We do not have a password associated with that email address. Please sign in as a new customer. ",
   "required.new_user_email_address" : "To create an account, please enter your email.",
   "removed_item.pro_giftcard" : "Pro Members are not eligible to purchase Gift Cards online at this time. ::skuname:: has been removed from your cart. Find <a style=\"color:#777777\" href=\"/locator/index.tmpl\">a M·A·C store</a> near you to purchase a Gift Card.",
   "required.name" : "Please enter your Kangi/Kana/ascii name.",
   "offer_minimum_purchase_amount" : "Sorry, the total value of your cart does not fulfill the requirements for offer code ::OFFER_CODE::.",
   "session_create_email" : "Please enter an email address to create an account.",
   "required.binary_file_wrong_type" : "The file is the wrong type.",
   "required.current_password" : "Please enter your current password.",
   "required.accept_terms" : "Please accept the Terms and Conditions in order to Create an Account.",
   "pro_restricted_access" : "You have attempted to access an area that is available only to M·A·C PRO Members as a benefit of membership. Please sign in below, or <a href=\"/macpro/cms/membership/howtoapply.tmpl\">click here</a> to learn how to become a member. If you feel you are receiving this message in error, please contact us at <a href=\"mailto:promembership@maccosmetics.com\">promembership@maccosmetics.com</a>",
   "mpp_long_out_of_stock" : "This item is currently not available. You will be kept informed via email.",
   "required.birth_month.macpro_member_registration" : "Please select your birth month",
   "phone" : "",
   "required.security_question_id" : "Please select a secret question.",
   "pwhint_contains_pw" : "Please use a hint that does not contain your password.",
   "giftcard.zero_balance_checkout" : "There is no value left on your gift card. The balance of your order will be charged to your credit card.",
   "cardmessage_toolong" : "Please limit the length of the gift card message to 280 characters. ",
   "required.binary_file" : "A file is required.",
   "required.birth_day.macpro_member_registration" : "Please select your birth day",
   "required.pc_email_address" : "Please enter your email address.",
   "required.first_name_alternate" : "Please enter your First Name kana.",
   "distinct.pc_email_address.mobile_email_address" : "Your PC email address and your mobile email address must be distinct.",
   "shipping_upgrade_pomilcan" : "Air Force Post Office / Fleet Post Office, for an order addressed to PO Boxes, or 2 business day delivery service, or next-day delivery service is temporarily unavailable.",
   "required.comments.macpro_contact_us" : "Please enter a comment.",
   "required.comments.email_an_artist" : "Please provide additional information.",
   "giftcard.vl_connect_failure" : "We were unable to connect to the Gift Card processor. Please try again later or contact a customer service representative.",
   "required.pc_oiliness" : "Please tell us about the oiliness or dryness of your skin.",
   "account_exists_signin" : "Our records indicate that you have an account with that email address. Please enter your password. ",
   "cart_merge_notice" : "Please review your order carefully as your shopping bag may contain some items from a previous visit.",
   "identical.new_password.new_password_verify" : "The new passwords you have provided do not match. Please re-enter your new password.",
   "required_dependency.email_address.pc_email_promotions.required_dependency.email_signup" : "Please check the email box.",
   "required.first_name" : "Please enter your First Name.",
   "cctype_ccnumber_mismatch" : "The credit card number you entered does not match the card type. Please  check that you have entered the correct  card type.",
   "offer_new_registered_only" : "Sorry, offer code ::OFFER_CODE:: is for new registered customers only.",
   "required.postal_code" : "Please enter your zip code.",
   "required.giftwrap" : "To include your personalized message, please click on the box under \\'Gift Wrapping.\\'. ",
   "hazmat" : "Please note this product cannot be shipped to a US Military address. Please supply a new shipping address or remove these items from your shopping bag.",
   "emailer_render_failed" : "Emailer failed to render. ",
   "unavailable.payment_type.cod.ship_name_ne_reg_name" : "We're sorry, but we are not able to send COD packages to adressees other than yourself.",
   "required.security_answer" : "Please enter the answer to your secret question.",
   "sms.cancel.subscription.invalid" : "You have already unsubscribed from receiving SMS updates.",
   "required.password" : "Please enter your password.",
   "birth_year" : "Please provide your birth year.",
   "required_and.sms_promotions.phone2.carrier_code.required_and.email_signup" : "Please select your mobile carrier.",
   "prodqty_sku" : " We're sorry, but we must limit orders to no more than ::QTY:: units of any item. The quantity of ::PRODUCTNAME:: has been reduced for you and added to your shopping bag.",
   "account_email_exists" : "Our records indicate that you have an account with that email address. Please enter your password. ",
   "required.binary_file_too_large" : "The file is too large.",
   "attempt_to_activate_existing_giftcard_id" : "**Sorry - we could not activate your eGift card because that Gift Card ID already exists.",
   "required.shipping_address" : "Please enter shipping information below.",
   "lipstick" : "Lipstick",
   "required.account_prefecture" : "Please select the prefecture for this address.",
   "signin_error" : " Sorry - we cannot sign you in. Either your email address or password is incorrect.",
   "pro_discount_none" : "Pro discount does not apply.",
   "giftcard_zero_balance" : "Please note that there is no value left on your gift card, thus it has been removed from your order.",
   "signin__error" : "We do not have a password associated with that email address. Please sign in as a new customer.",
   "account_lockout" : "Your account has been locked. To cancel, please contact customer service 1-800-588-0070.",
   "required.address1" : "Please enter the street address",
   "cart.merge" : "Please review your order carefully as your shopping bag may contain some items from a previous visit.",
   "required.housing_number.macpro_contact_us" : "Please enter your house number.",
   "distinct.password.new_password" : "Your new password cannot be your temporary password. Please enter a different password.",
   "multiset_limit_exceeded" : "You've selected more than the allowed number of sample items. Please select ::variable (sample max):: samples.",
   "required.ship_last_name" : "Please supply a Kangi/Kana/ascii Last Name.",
   "cc_mismatch" : "The credit card number you entered does not match the card type. Please  check that you have entered the correct  card type.",
   "identical.new_password.new_password2" : "The new passwords you have provided do not match. Please re-enter your new password.",
   "no_from_address" : " No from address specified for emailer.",
   "required_dependency.sms_promotions.carrier_code" : "Please select your mobile carrier.",
   "required.shade1.giving_back" : "Please select your first shade preference.",
   "format.pro_id.macpro_contact_us" : "Please enter a valid Pro Membership ID in this format (12#########)",
   "sms.subscribe.subscription.already_active" : "You are already subscribed.",
   "cart.empty" : "Your shopping bag cart does not contain priced items. ",
   "email_address_verify" : " The Email Address fields must be identical.",
   "sku_unavailable" : ": ::variable::  is no longer available, and has been removed from your cart.",
   "required.card_number" : "Please enter your credit card number.",
   "csz_z" : "Your zip/postal code does not match your city and state/province.",
   "max" : "",
   "invalid_mime_error" : "That file type is not supported.",
   "required.valid_phone" : "Please enter your phone number.",
   "required.address" : "Please enter your address.",
   "limit.gc_balance" : "Please call 1.800.588.0700 for help with your gift card balance.",
   "unsupported_shipping_address" : "We’re sorry, but this order cannot be shipped to a Canada address.  Please supply a US shipping address.",
   "required.password.password_reset" : "Please enter your temporary password.",
   "ca_addr_not_country" : "You entered a Canada address, but did not select Canada as your country.",
   "required.accept_privacy" : "Please accept the Privacy Policy in order to Create an Account.",
   "required.card_type" : "Please select a payment type.",
   "no_shipmethod" : "Please select a shipping method.",
   "length_min.new_password" : "Please supply a password that is at least 6 characters long.",
   "address_has_cc" : "You have at least one credit card associated with this address. Click here to make changes to your billing information",
   "holiday_newyears_shipping" : "Please Note:  Due to the New Year's Holiday and holiday shipping schedules, orders placed after 3:00 PM EST on Monday, December 31 will be processed on Tuesday, January 2.",
   "user.query_by_email_address.email_address_required" : "Please enter your email address in the following format: jane@aol.com",
   "required.user_type.macpro_artist_email" : "Please select your User Type.",
   "offer_one_time_use_only_redeemed" : "This offer code has already been redeemed and is no longer valid.",
   "email_taken" : "Our records indicate that this email address already has an account associated with it. You may either use another email address or click here to go to the sign in page",
   "required.ship_prefecture" : "Please select the prefecture for this address.",
   "reset_password_not_signed_in_user" : "Please sign in to reset your password.",
   "required.member_number" : "Please enter a valid Membership Number.",
   "giftcard_used_for_giftcard_only" : "We're sorry, but you may not use a gift card for gift-card-only orders.",
   "taken.pc_email_address" : "An account already exists with that email address. Please select a different email address.",
   "sku_unavailable_7" : "Unfortunately, ::variable::  is sold out, and has been removed from your cart.",
   "required.account_first_name2" : "Please enter your First Name kana.",
   "birth_month" : "Please provide your birth month.",
   "required phone" : "",
   "auto_fss_met" : "You have qualified for free standard shipping!",
   "invalid.password.checkout~checkout_signin" : "We do not recognize your sign in information. Please try again. Please note the password field is case sensitive.",
   "email_signup_other_account_exists" : " An account already exists with that email address.  Please select a different email address if you would like to subscribe to the email newsletter.",
   "mpp_sold_out" : "This product is sold out and is no longer available online.",
   "zp_z" : "Your postal code does not match your city and state.",
   "required.ship_street" : "Please enter the street/block no.",
   "cart_out_of_stock" : "Temporarily out of stock. You may order this product and we will ship it to you as soon as it is back on stock. We will keep you informed via email.",
   "format.card_message_to" : "We're sorry. A character you have entered is not able to be accepted. Please use only English or Spanish characters for gift messages.",
   "full_addr_required" : "Please provide a full address, including a phone number. ",
   "update_change_unsubscribe_collistion" : "We're sorry. We cannot allow you to unsubscribe and change the email address at the same time.",
   "required_dependency.pc_email_promotions.email_address.required_dependency.waitlist_email_signup" : "Please complete the missing information.",
   "required_dependency.email_address.pc_email_promotions" : "Please check the email box.",
   "general_upload_error" : "Oops, something bad happened on the file upload. Please try again.",
   "required.ship_method" : "Please select a ship method. ",
   "offer_group_redeemed" : "This offer code has already been redeemed and is no longer valid.",
   "account_city_error" : "Your Address city/town does not match your zip code.",
   "addr_book_delete" : "That address has been deleted from you Address Book.",
   "signout_msg" : " You have been signed out.",
   "required_dependency.sms_promotions.phone2.carrier_code.registration" : "",
   "required_dependency.pc_email_promotions.pc_email_address.required_dependency.checkout_guestuser_contactinfo" : "Please provide your email address so we can send you a copy of your order information.",
   "illegal_from_address" : "Email cannot send from address.",
   "ship_city_error" : "Your Address city/town does not match your prefecture and postal code.",
   "required.shade3.giving_back" : "Please select your second shade preference.",
   "invalid.password.signin" : "We do not recognize your sign in information. Please try again. Please note the password field is case sensitive.",
   "required.account_last_name" : "Please enter your Last Name.",
   "min" : "",
   "account_update_signin" : "After logging on, please update your information.",
   "signedin.required" : "Please sign in below.",
   "required.new_password" : "Please supply your new password.",
   "question" : "Comments / Please enter your message.",
   "no_default_payment_address_saved" : "!Please add or choose an address at right.",
   "incorrect_pwremind" : "We do not have a password associated with that email address. Please sign in as a new customer.",
   "offer_restrict_country" : "Sorry, offer code ::OFFER_CODE:: is only available in: ::COUNTRY::.",
   "sweepstakes_signin" : "Please sign in to enter the sweepstakes.",
   "collection.signedin_or_recognized.required" : "Please log in below",
   "required_dependency.carrier_code.sms_promotions" : "Please agree to the mobile terms and conditions",
   "user.query_by_email_address.email_address_invalid" : "Please enter your email address in the following format: jane@aol.com",
   "required.shade2.giving_back" : "Please select your third shade preference.",
   "addr_cc_deleted" : "Your credit card has been deleted.",
   "offer_ineligible" : "Sorry, you are not eligible to use offer code ::variable::. Please see our offers page for more information.",
   "restricted.ship_method.ak" : "Second Day and Overnight Service are not available for orders going to Alaska.",
   "required.house_number" : "The House Number field is required.",
   "pro_restricted" : "Only PRO members can signin to the MAC PRO site. If you wish to sign in to MAC Cosmetics, please click <a href=\"/account/signin.tmpl\">here</a>.",
   "required.ship_address_type" : "Please select the type of address.",
   "required.locator_country" : "Please select a country.",
   "required.account_last_name2" : "Please enter your Last Name kana.",
   "required.offer_giftwrap" : "Sorry, offer code ::OFFER_CODE:: requires that your order be giftwrapped.",
   "cc_expired" : "The credit card below has expired. Please edit this credit card or add a new one.",
   "offer_criteria_met" : "Offer code ::offer_code:: has been successfully applied.",
   "not_allowed.hazmat.address" : "You have products in your cart which may not be sent to an APO address. Please enter a new address, or remove any items below which are marked as not available for delivery to an APO/FPO address.",
   "delivery_restriction.hazmat.item" : "You have products in your cart which may not be sent to an APO address. Please enter a new address, or remove any items below which are marked as not available for delivery to an APO/FPO address.",
   "restricted.ship_method.hi" : "Second Day and Overnight Service are not available for orders going to Hawaii.",
   "required.password_hint" : "Please enter a password hint.",
   "removed_item.out_of_stock" : "::skuname:: is out of stock and has been removed from your cart.",
   "required_dependency.mobile_email_promotions.mobile_email_address" : "Please enter a mobile email address to receive mobile newsletters.",
   "reset_password_same_password" : "Your new password must be different from your current password.",
   "format.phone2" : "Please enter a phone number that is 10-12 numbers and does not start with a 0 or 1.",
   "required.checkout_email_address_verify" : "The Email Addresses you have provided do not match. Please re-enter your email address.",
   "day_telephone" : "*Day Telephone",
   "fu2_nonstandard_method" : "You entered an offer code for a shipping upgrade, but you did not select standard shipping.",
   "format.postal_code.address" : "Please enter a 5 digit zip code for US addresses or in the following format for CA addresses: Z9Z 9Z9.",
   "migrated.mobile_account.signin" : "If you do not have a password (previous mobile users, etc), please create an account.",
   "required.pro_id.contact_us" : "Please enter your Pro membership ID.",
   "required.account_first_name" : "Please enter your First Name.",
   "required.locator_state_province" : "Please select a state/province.",
   "required.ship_first_name" : "Please supply a Kangi/Kana/ascii First Name.",
   "select_a_giftcard" : "Please select a Gift Card Value.",
   "account_prod_discontinued" : "This product has been discontinued and is no longer available online.  We recommend ::variable::.",
   "required.city" : "Please enter the city/town for this address.",
   "shipmethod_set_to_default" : "We apologize for any inconvenience. Currently only standard shipping is available for your order. We have changed this automatically for you. If you have any questions please contact us at 1-800-588-0070.",
   "offer_fu2_used_overnight" : "This offer code is for Free 2nd Day Shipping.  Please re-enter your offer code and select 2nd Day Service.",
   "required.pc_lines" : "Please tell us about your skins surface lines.",
   "required.membership_number.admin_po_validate_pro" : "The Pro member ID does not match what we have on record.",
   "account_postalcode_error" : "Your Address zip code does not match your city/town.",
   "please_signin" : "Please sign in below. ",
   "duplicate_username.custom_username.macpro_member_registration" : "That nickname is already in use. Please select another nickname.",
   "invalid.password2.registration" : "Please enter a password that is 6-12 characters.",
   "required.ship_first_name2" : "Please supply your Katagana First Name.",
   "required.locator_city" : "Please select a city.",
   "spp_out_of_stock" : "Temporarily out of stock. You may order this product and we will ship it to you as soon as it is back on stock. We will keep you informed via email.",
   "badqty" : "Please enter a number into the quantity box to purchase an item. ",
   "holiday_xmas_shipping" : "Please Note:  Due to the Christmas Holiday and holiday shipping schedules, orders placed on Monday, December 24 will be processed on Wednesday, December 26. Our Customer Service Center will close at 5:00 PM EST on December 24 and will reopen at 8:00 AM EST on December 26 to allow our staff to spend the holiday with their families. Happy Holidays!",
   "lostpw_retrieved_msg" : " We have sent your password to your email address.",
   "not_checkout_offer" : "Sorry, offer code ::OFFER_CODE:: is not for use at checkout.",
   "required.college_name" : "A College Name is required.",
   "required.pc_email_address2" : "Please re-enter your email address.",
   "identical.email_address.email_address_verify.identical.waitlist_email_signup" : "The Email Address fields must be identical.",
   "nonunique_shipoffer" : "Please note only one offer code can be accepted per order.",
   "address.not_found" : "We're sorry, but there was an issue with one of your addresses. Please contact customer service.",
   "distinct.new_password.password_hint" : "Your new password cannot be your password hint. Please try again.",
   "email_signup_email_one_type_required" : "You must indicate at least one choice of email you would like to receive.",
   "format.postal_code" : "",
   "removed_item" : "::skuname:: has been removed from your cart.",
   "gc_no_value" : "There is no value left on your gift card. The balance of your order will be charged to your credit card.",
   "required.phone1.giving_back" : "Please provide a phone number.",
   "invalid.password.macpro_signin" : "We do not recognize your sign in information. Please try again. Please note the password field is case sensitive.",
   "account_prefecture_error" : "Your State/Province does not match your city/town and postal code.",
   "required.account_zip" : "Please enter a zip code.",
   "email_only_acct_found" : " This feature is only provided to registered members.",
   "gift_card_value" : "Please select a gift card value.",
   "invalid.new_password.password.rest" : "Please supply a password that is at least 6 characters long.",
   "required_and.password.password2.password_hint" : "If you wish to create an account, please enter a password, verify password and password hint.",
   "required..zip_for_event" : "Please provide your zip code so we may notify you of [BRAND] events in your area.",
   ".membership_number.admin_po_validate_pro" : "We could not validate the Pro membership account information.",
   "required.retpack3.giving_back" : "Please select your third return product.",
   "distinct.password.password_hint" : "Your password cannot be your password hint. Please try again.",
   "obscene.custom_username.macpro_registration" : "Bad language filter. Please enter a new screenname.",
   "format.gc_giftcard_number.giftcard_balance" : "Your Gift Card Number or PIN not recognized. Please re-enter the numbers in the Gift Card number and PIN fields.",
   "identical.pc_email_address.pc_email_address2" : "The Email Address fields must be identical.",
   "prodqty" : " The product quantity for an item in your Shopping Bag has exceeded our Maximum Purchasing Policy. The product quantity is now adjusted to meet our policy maximum. For details, refer to our <a href=\"#\" onClick=\"popWin('/customerservice/cs_max_popup.tmpl', 'max', '516', '250', 'yes')\">Maximum Purchasing Policy</a>.",
   "required.pc_haircolor" : "Please tell us the color of your hair.",
   "bad_cc_number" : "The credit card number you entered is not valid. Please  check that you have entered the correct  number.",
   "invalid_offer_code" : "The offer code ::offer_code:: is not valid.",
   "pro_discount" : "Pro discount is available on select items.",
   "us_addr_not_country" : "You entered a US address, but did not select US as your country.",
   "required.pc_poresize" : "Please tell us your pore size.",
   "required.pc_breakout" : "Please tell us your breakout tendency.",
   "required.gc_giftcard_number.gc_giftcard_pin" : "Your Gift Card Number or PIN not recognized. Please re-enter the numbers in the Gift Card number and PIN fields.",
   "cart_long_out_of_stock" : "Unfortunately, products that are temporarily out of stock at the time of purchase are not eligible for promotional offers.",
   "email_update_account_not_found" : "Sorry - we could not find a record matching the information you provided. ",
   "required.ship_last_name2" : "Please supply your Katagana Last Name.",
   "required.billing_address_id" : "Please select a billing address.",
   "required.custom_username.macpro_member_registration" : "Please enter a nickname.",
   "required.retpack1.giving_back" : "Please select your first return product.",
   "required.locator_store_type" : "Please select at least one store type.",
   "identical.new_password.new_password_verify.identical.password_reset" : "The new passwords you have provided do not match. Please re-enter your new password.",
   "delivery_options" : "Delivery Method",
   "incorrect.security_answer" : "Wrong answer your secret question. Please enter the correct details.",
   "spp_sold_out" : "This product is sold out and is no longer available online.",
   "message" : "Please limit the length of the giftcard message to 215 characters.",
   "required.email_address" : "Please enter your email address.",
   "unable_to_retrieve_egiftcard" : "We are currently unable to retrieve your eGift Card. Please contact a customer service representative.",
   "required.pro.password_match_existing_acct" : "An account on maccosmetics.com already exists for that email address. Please re-enter a valid password or choose a new email address to be associated with your M·A·C Pro account.",
   "min_max.phone2.registration" : "Please enter a phone number that is 10-12 numbers and does not start with a 0 or 1.",
   "addr_book_saved_changes" : "The changes you made have been saved.",
   "format.phone1" : "Please enter a phone number that is 10-12 numbers and does not start with a 0 or 1.",
   "rbe_version" : 0.2,
   "account_long_out_of_stock" : "This item is currently not available. You will be kept informed via email.",
   "offer_minimum_purchase_items" : "The minimum requirement for redeeming this offer code has not yet been met.",
   "no_shipping_info" : "Please provide your shipping information below. ",
   "invalid.password" : "Please enter a password with 6-12 alphanumeric characters.",
   "required.new_password2" : "Please re-enter your new password.",
   "offer_sample_out_of_stock" : "Sorry, ::skuname:: provided by offer code ::offer_code:: is no longer available.",
   "sku_already_in_collection.add_sku.collection" : " was already added to your favorites.",
   "session" : "",
   "restricted.ship_method.us_territory" : "Second Day and Overnight Service are not available for orders going to US Territories.",
   "cart.qty_limit" : "The product quantity can not exceed ::qty_limit:: per order. Please review our <a href=\"/cms/customer_service/shoponline.tmpl#max_purchase\"><font color=\"#333333\">Maximum Purchasing Policy</font></a> for more details.",
   "signin_update" : " You must be signed in to update your registration.",
   "invalid.card_number" : "Please enter a valid credit card number.",
   "auto_fss_unmet" : "You are only ::needs:: away from receiving free standard shipping!",
   "account_exists" : "Our records indicate that you have an account with that email address. Please enter your password. ",
   "mpp_out_of_stock" : "Temporarily out of stock.  You may order this product and we will ship it to you as soon as it is back on stock. We will keep you informed via email.",
   "length_min.password" : "Please enter a password hint 6 or more characters.",
   "mobile_opt_in_terms" : "I'd like to hear from M·A·C Cosmetics Online via sms and mms. I agree with the <a href=\"javascript:void();\" id=\"terms-\\ popup-link\">Mobile Terms and Conditions.</a>",
   "mismatch.card_type.card_number" : "Your credit card type does not match your credit card number.",
   "mpp_discontinued" : "This product has been discontinued and is no longer available online.",
   "cart_limit" : "There is a maximum of $XXX per order.  Please remove some items from your cart. For details, refer to our <a href=\"#\" onClick=\"popWin('/customerservice/cs_max_popup.tmpl', 'max', '516', '250', 'yes')\">Maximum Purchasing Policy</a>.",
   "required.retpack4.giving_back" : "Please select your fourth return product.",
   "required.full_name" : "Please enter your name.",
   "account_signin_to_edit" : "Please sign in to update your registration profile.",
   "order_review_problem" : "Please review your order and correct any problems. ",
   "expired_offer_code" : "Your offer code has expired.",
   "min_max.phone2.email_signup" : "Please enter a phone number that is 10-12 numbers and does not start with a 0 or 1.",
   "required.expires_month" : "Please supply the month when your credit card expires. ",
   "invalid.email_address" : "Please enter your email address in the following format: jane@aol.com",
   "ship_postalcode_error" : "Your Address postal code does not match your city/town and prefecture.",
   "required.last_name_alternate" : "Please enter your last name Kana.",
   "sku_unavailable_now" : "This item is currently not available. You will be kept informed via email. This item will be available in ::variable::",
   "required.title" : "Please select your title.",
   "required.pro_id.contact_us required.pro_id.contact_us" : "Please enter your Pro member id.",
   "one_time_use.offer_criteria_not_met" : "The offer code ::offer_code:: has already been redeemed and is no longer valid.",
   "signin_modaddr" : " You must be signed in to modify your address book.",
   "addr_book_confirm_delete" : "Are you sure you want to delete this address? Yes | No",
   "no_cc_info" : "Please provide your payment information below.",
   "password.hint" : "Please enter your email address to view your password hint",
   "cc_type_id" : "Please select the type of Credit Card. ",
   "identical.password.password2" : "The passwords you have provided do not match. Please re-enter your password.",
   "required.email_address2" : "Please re-enter your email address for verification.",
   "required.account_address" : "Please enter your address.",
   "required.pro_id.macpro_contact_us" : "Please enter your Pro membership ID.",
   "csz_x" : "We could not validate your address.",
   "format.card_message_from" : "We're sorry. A character you have entered is not able to be accepted. Please use only English or Spanish characters for gift messages.",
   "required.expires_year" : "Please supply the year when your credit card expires.",
   "required.customer_type.contact_us" : "Please tell us if you are a US, Canada, or Pro customer.",
   "empty_cart" : "Cart contains no priced items. ",
   "required.retpack2.giving_back" : "Please select your second return product.",
   "required.last_name" : "Please enter your Last Name. ",
   "addr_deleted" : "That address has been deleted from your address book. ",
   "birth_day" : "Please provide your birth day.",
   "duplicate_username.custom_username.macpro_registration" : "That nickname is already in use. Please select another nickname.",
   "new_password_verify" : "The new passwords you have provided do not match. Please re-enter your new password.",
   "required_dependency.macpro_email_promotions.pc_email_address.required_dependency.macpro_member_registration" : "Your email address is required",
   "required.housing_number.contact_us" : "Please enter your house number.",
   "offer_cc_type_id" : "Sorry, offer code ::OFFER_CODE:: is limited to ::REQUIRED_CARD:: customers.",
   "invalid.new_user_email_address" : "Please enter an email address in the following format: jane@aol.com.",
   "required.regarding.macpro_artist_email" : "Please select what your question is regarding.",
   "password_tooshort" : "Please supply a password that is at least 6 characters long.",
   "invalid_emial" : "Please enter your email address in the following format: jane@aol.com ",
   "select_a_prod" : " Please select a product size below.",
   "account_prod_out_of_stock" : "Temporarily out of stock.  You may order this product and we will ship it to you as soon as it is back on stock. We will keep you informed via email.",
   "ship_prefecture_error" : "Your Address prefecture does not match your city/town and postal code.",
   "required.custom_username.macpro_registration" : "Please enter a nickname.",
   "offer_one_time_use_only_ineligible" : "XXX promotion code is not available. For more information, please visit our discount page.",
   "required_or.pc_email_address.mobile_email_address" : "Please enter either a PC email address or a mobile email address. ",
   "ecard_recipient_info" : "You must provide complete information for all ecard recipients.",
   "max_char_exceeded" : "You have exceeded the maximum number of characters.",
   "required.account_city" : "Please enter the city/town for this address.",
   "update_signup_email_updating_found_email" : "We cannot change your email address. The email address you selected belongs to an account that already exists in our records.",
   "invalid_zip.zip" : "Your zip code does not match your city and state.",
   "invalid_email" : "Please enter an email address in the following format: jane@aol.com.",
   "success.add_sku.collection" : " was added to your favorites.",
   "no_qty" : " Please enter a number into the quantity box to purchase an item.",
   "required_dependency.phone2.carrier_code" : "Please select your mobile carrier.",
   "account_email_updated" : "Thanks for visiting BRAND.COM.  Per your request, the e-mail address associated with your account has been changed. The old address was ::variable::. The new address is ::variable::.",
   "invalid.mobile_email_address.registration" : "Please enter a valid mobile email address.",
   "spp_discontinued" : "This product has been discontinued and is no longer available online.",
   "csz_c" : "Your Address city/town does not match your state and zip code.",
   "invalid_password.. invalid_password" : "Please review and re-enter your temporary password.",
   "offer_one_time_use_only" : "This offer code has already been redeemed and is no longer valid.",
   "account_updated" : "Your personal information has been updated.",
   "required.pc_skincolor" : "Please tell us your skin color.",
   "csz_s" : "Your State/Province does not match your city and zip/postal code.",
   "removed_item.sold_out" : "::skuname:: is sold out and has been removed from your cart.",
   "restricted.ship_method.military" : "Second Day and Overnight Service are not available for orders being sent to APO/FPO addresses or P.O. Box addresses.",
   "offer_giftcard_required" : "The ::OFFER_CODE:: offer is valid only when redeeming a [BRAND] gift card.  Please enter a gift card number and pin in order to use this offer.",
   "required.password2" : "Please re-enter your password for verification.",
   "changes_saved" : "Your account information has been saved.",
   "locale_switch" : "Please note that products prices, promotions, taxes and ship methods may vary between shipments to US or Canada. Please review your order summary with this in mind.",
   "invalid.password.checkout" : "We do not recognize your sign in information. Please try again. Please note the password field is case sensitive.",
   "identical.email_address.email_address2" : "The email addresses you have provided do not match. Please re-enter your  email address.",
   "macpro_account_not_found" : "The membership information you entered is not valid. Please review your information and try logging in again. If you continue to experience difficulty logging in to the site, please contact us at <a href=\"mailto:promembership@maccosmetics.com\">promembership@maccosmetics.com</a>.",
   "required.ship_phone" : "Please enter your phone number.",
   "required.new_password_verify" : "Please re-enter your new password.",
   "giftcard_error" : "There is a problem with your Gift Card info. Please see below."
};
        
            rb.endeca = {
   "review" : "review",
   "show_all" : "Show all »",
   "search_results" : "Search Results",
   "shop_all_shades" : "Shop All Shades",
   "did_you_mean" : "Did you mean #{didYouMean}?",
   "search_result_term" : "Your search for \"#{searchTerm}\" returned the following results.",
   "no_results" : "No items found matching \"#{searchTerm}\".",
   "no_term" : "Please enter a keyword in the search box on the left side of this page and click SEARCH.",
   "search_again" : "Search again",
   "products" : "Products",
   "no_results_suggestion" : "We suggest you check for typos, broaden your search and enter the type of product you need.",
   "articles" : "Articles",
   "reviews" : "reviews",
   "no_filters" : "No Filters Available.",
   "view_details" : "View Details",
   "no_products" : "There are no product results for your search.",
   "view_all" : "View all",
   "search_within" : "Search within these results"
};
        

var jsTemplates = {
    globalnav : {
           Accordion: '<li id="#{id}"><img id="#{id}_hd" src="#{hdPath}" alt="#{displayName}" attachEvent="mouseover:_onMouseOver,mouseout:_onMouseOut,click:onClick" class="accordion_hd clickable"><br /><ul id="#{pnavsetChild}" class="accordion_content" style="display:none;" attachPoint="containerNode"></ul></li>          ',
           ArtistryInActionSubNav: '<div id="#{id}" class="panelnav_subnav panelnav_detail_container">    <div attachPoint="progressNode" class="progress"><br></div>    <div attachPoint="containerNode" class="invisible">        <div attachPoint="featuredContainerNode">               </div>        <div class="psubnav_artistry_in_action_btn_container"><a href="#" id="psubnav_artistry_in_action_btnprevious"><img alt="PREVIOUS" src="/images/pnav/headers/pnav_artistryinaction_previous_off.gif" /></a></div>        <div attachPoint="previousContainerNode" class="hidden">               </div>    </div></div>',
           Panel: '<div class="panel" id="#{id}">    <div class="panelnav_container clearfix">        <div id="#{id}_close" attachEvent="click:_onClickClose" class="closelight">x</div>        <div attachPoint="containerNode"></div>    </div></div> ',
           PanelNav: '<li id="#{id}" class="globalnav_hd clickable" attachPoint="containerNode"><img id="#{id}_hd" src="#{hdPath}" alt="#{displayName}" attachEvent="mouseover:_onMouseOver,mouseout:_onMouseOut,click:_onClick"></li>',
           PanelSubNav: '<div id="#{id}" class="panelnav_subnav panelnav_detail_container">    <div attachPoint="progressNode" class="progress"><br></div>    <div attachPoint="containerNode" class="invisible">           </div></div>',
           ProductSubNav : '<div id="#{id}" class="panelnav_subnav">    <div attachPoint="progressNode" class="progress"><br></div>    <div attachPoint="containerNode" class="invisible">        <div id="#{id}_links" class="panelnav_detaillink_container" attachPoint="detailLinksContainerNode">            </div>        <div id="#{id}_cat" class="panelnav_detail_container" attachPoint="detailContainerNode">            </div>        <ul id="#{id}_catlist" class="panelnav_accordion_container hidden" attachPoint="accordionContainerNode">        </ul>    </div></div>',
           SearchSubNav : '<div id="#{id}" class="panelnav_subnav panelnav_detail_container">    <div attachPoint="resultsMessageNode" class="search_results_message"></div>    <div class="search_results_hd"><img id="#{id}_hd" src="/images/search/h_top_searches#{imgSuffix}.gif" alt="" /></div>    <div attachPoint="progressNode" class="progress"><br></div>    <div attachPoint="contentResultsContainer" class="hidden search_content_results"><div attachPoint="contentResultsNode"></div>    </div>    <div attachPoint="resultsNode">    </div>    <!--<div id="search_result_pages"></div> note: was result_pgs--></div>',
           DiscontinuedSubNav : '<div id="#{id}" class="panelnav_subnav panelnav_detail_container">    <div attachPoint="progressNode" class="progress"><br></div>    <div attachPoint="containerNode" class="invisible">        <img id="#{id}_hd" class="panelnav_disc_hd" src="/images/goodbyes/headers/h_discontinued_prods#{imgSuffix}.gif" width="250" alt="Goodbyes" />        <p attachPoint="panelDescriptionNode" class="panelnav_disc_descr"></p>        <div attachPoint="featuredNode">        </div>                <!--<div id="discontinued_search">            <img src="/images/pnav/category/headers/pnav_search_disc_prods_250x18_off#{imgSuffix}.gif" width="250" height="18" alt="Search Discontinued Products" id="disc_search_hd">            <p attachPoint="searchDescriptionNode"></p>            <form onsubmit="return false;">                <input type="text" id="disc_search_input" class="text_field" />                <input type="image" class="btn" id="disc_search_submit" src="/images/forms/btn_input.gif" />            </form>            <div id="disc_search_progress" class="progress"><br></div>            <div class="clearfix"></div>        </div>-->                <ul attachPoint="resultsNode" class="search_results">        </ul>        <div attachPoint="resultsMessageNode" class="search_results_message"></div>    </div></div>',
           DiscontinuedProductDetail : '<li id="#{id}" class="panelnav_link" attachEvent="mouseover:_onMouseOver,mouseout:_onMouseOut,click:_onClick">    <a href="#{url}">        <div class="panelnav_detail">            <div class="panelnav_detail_text">                <h3><img id="#{id}_hd" src="#{hdPath}" alt="#{displayName}" /></h3>                <p attachPoint="shadenameNode" class="panelnav_shadename hidden">#{shadename}</p>                             <p attachPoint="descriptionNode">#{description}</p>            </div>            <div class="smoosh_small" style="background-color: #{hex};"><img class="thumb" src="#{thumbPath}" width="56" height="56" alt="#{displayName}" /></div>        </div>    </a></li>',
           SectionDescSubNav : '<div id="#{id}" class="panelnav_subnav panelnav_detail_container">    <div attachPoint="progressNode" class="progress"><br></div>        <div attachPoint="contentNode" class="invisible">        <img attachPoint="hdNode" class="panelnav_section_hd" width="250" />        <p attachPoint="panelDescriptionNode" class="panelnav_section_descr"></p>        <div class="panelnav_detail_container" attachPoint="containerNode">            </div>        </div></div>',
           Detail: '<li id="#{id}" class="panelnav_link panelnav_detailItem #{baseClass}" attachEvent="mouseover:_onMouseOver,mouseout:_onMouseOut,click:_onClick">    <a href="#{url}"><div class="panelnav_detail">        <div class="panelnav_detail_text">            <h3>            <img id="#{id}_hd" src="#{hdPath}" alt="#{displayName}" class="panelnav_detail_hd" /></h3>            <p>#{description}</p>        </div>        <div class="panelnav_thumb"><img id="#{id}_thumb" src="#{thumbPath}" width="56" height="56" alt="" /></div>    </div></a></li>',
           SimpleDetail : '<li id="#{id}" class="panelnav_link panelnav_detailItem #{baseClass}" attachEvent="mouseover:_onMouseOver,mouseout:_onMouseOut,click:_onClick">    <a href="#{url}"><div class="panelnav_detail nothumb">        <div class="panelnav_detail_text">            <h3><img id="#{id}_hd" src="#{hdPath}" class="panelnav_detail_hd" alt="#{displayName}" /></h3>            <p>#{description}</p>        </div>        <div class="panelnav_hspacer"><br /></div>    </div></a></li>',
           CollectionCategoryDetail : '<div id="#{id}"><div id="#{id}_cat" attachPoint="categoryDetailNode" class="panelnav_link" attachEvent="mouseover:_onMouseOver,mouseout:_onMouseOut,click:_onClick"><div class="panelnav_detail"><div class="panelnav_detail_text"><h3><img id="#{id}_hd" src="#{hdPath}" style="height: 18px;" alt="#{displayName}" /></h3><p>#{description}</p></div><div class="panelnav_thumb"><img src="#{thumbPath}" width="56" height="56" alt="" /></div></div></div><div class="clear"><br /></div><ul id="#{id}_catlist" class="hidden" attachPoint="accordionContainerNode"></ul></div>',
           ProductCategoryDetail : '<div id="#{id}" class="panelnav_link" attachEvent="mouseover:_onMouseOver,mouseout:_onMouseOut,click:_onClick">    <div class="panelnav_detail">        <div class="panelnav_detail_text">            <h3><img id="#{id}_hd" src="#{hdPath}" alt="#{displayName}" class="panelnav_catdetail_hd" /></h3>            <p>#{description}</p>        </div>        <div class="panelnav_thumb"><img src="#{thumbPath}" width="56" height="56" alt="" /></div>    </div></div>',
           SearchQuickBuyDetail : '<div id="#{id}" class="panelnav_link panelnav_link_quickbuy panelnav_detailItem" attachEvent="mouseover:_onMouseOver,mouseout:_onMouseOut">    <div class="panelnav_detail">        <div class="panelnav_detail_text">            <h3><a href="#{url}"><img id="#{id}_hd" src="#{hdPath}" alt="#{displayName}" class="panelnav_detail_hd" /></a></h3>            <a href="#{url}">            <p attachPoint="shadenameNode" class="panelnav_shadename hidden">#{shadename}</p>            <p attachPoint="descriptionNode" class="hidden">#{description}</p>            </a>            <input type="image" src="/images/products/btn/btn_add_to_bag_93#{imgSuffix}.gif" id="#{id}_btn_add" value="" class="panelnav_btn_add" />            <span attachPoint="inventoryStatusNode" class="inventory_status"></span>        </div>        <div class="smoosh_small" style="background-color: #{hex};"><a href="#{url}"><img class="thumb" src="#{thumbPath}" alt="#{displayName}" /></a></div>        <div attachPoint="cartConfirmNode"></div>    </div></div>',
           SearchProductDetail : '<div id="#{id}" class="panelnav_link panelnav_link_search panelnav_detailItem" attachEvent="mouseover:_onMouseOver,mouseout:_onMouseOut,click:_onClick">    <a href="#{url}">        <div class="panelnav_detail" attachPoint="panelDetailNode">            <div class="panelnav_detail_text">                <h3><img id="#{id}_hd" src="#{hdPath}" alt="#{displayName}" /></h3>                <p>#{description}</p>                <img id="#{id}_actionimg" src="/images/search/btn_view_shades_off#{imgSuffix}.gif" class="panelnav_btn_view_shades">            </div>            <div class="smoosh_small" style="background-color: #{hex};"><img src="#{thumbPath}" width="56" height="56" alt="#{displayName}" /></div>        </div>    </a></div>',
           SearchGiftcard : '<div class="panelnav_link panelnav_link_search" attachEvent="mouseover:_onMouseOver,mouseout:_onMouseOut,click:_onClick">    <a href="#{url}">        <div class="panelnav_detail" attachPoint="panelDetailNode">            <div class="panelnav_detail_text">                <h3><img id="#{id}_hd" src="#{hdPath}" alt="#{displayName}" /></h3>                <p>#{description}</p>                <img src="/images/search/btn_view_select_value_off#{imgSuffix}.gif" class="panelnav_btn_view_shades" attachPoint="actionImgNode">            </div>            <div class="smoosh_small" style="background-color: #{hex};"><img src="#{thumbPath}" alt="#{displayName}" /></div>        </div>    </a></div>',
           SearchCustomPalette : '<div class="panelnav_link panelnav_link_search" attachEvent="mouseover:_onMouseOver,mouseout:_onMouseOut,click:_onClick">    <a href="#{url}">        <div class="panelnav_detail" attachPoint="panelDetailNode">            <div class="panelnav_detail_text">                <h3><img id="#{id}_hd" src="#{hdPath}" alt="${displayName}" /></h3>                <p>#{description}</p>                <img src="/images/search/btn_custom_palette_off#{imgSuffix}.gif" class="panelnav_btn_view_shades" attachPoint="actionImgNode">            </div>            <div class="smoosh_small" style="background-color: #{hex};"><img src="#{thumbPath}" alt="#{displayName}" /></div>        </div>    </a></div>',
           ContentSearchDetail : '<div id="#{id}" class="panelnav_link panelnav_link_search_content" attachEvent="mouseover:_onMouseOver,mouseout:_onMouseOut,click:_onClick">    <a href="#{url}"><div class="panelnav_detail nothumb">        <div class="panelnav_detail_text">            <p>#{description}</p>        </div>        <div class="panelnav_hspacer"><br /></div>    </div></a></div> \n',
           headerLi : '<li id="#{id}" class="link_hd clickable" attachpoint="containerNode">    <a href="#{url}"><img src="#{hdPath}" alt="#{displayName}" attachPoint="hdNode"></a></li> \n',
           headerDiv : '<div id="#{id}" class="link_hd clickable" attachpoint="containerNode">    <a href="#{url}"><img src="#{hdPath}" alt="#{displayName}" attachPoint="hdNode"></a></div> \n',
           VideosSubNav: '<div id="#{id}" class="panelnav_subnav panelnav_detail_container">    <div attachPoint="progressNode" class="progress"><br></div>    <div attachPoint="contentNode" class="invisible">        <img attachPoint="hdNode" class="panelnav_section_hd" width="250" />        <p attachPoint="panelDescriptionNode" class="panelnav_section_descr"></p>        <div attachPoint="containerNode"><div attachPoint="featuredContainerNode">       </div><div class="psubnav_artistry_in_action_btn_container"><a href="#" id="psubnav_artistry_in_action_btnprevious"><img alt="PREVIOUS" src="/images/pnav/headers/pnav_artistryinaction_previous_off.gif" /></a></div><div attachPoint="previousContainerNode" class="hidden">       </div></div>    </div>    </div> \n'
    },
    product : {
           cartAdd : '<div id="#{id}" class="overlay-container cart-add-overlay-container">    <div class="overlay-content-popover popover-prod">        <div class="close-container"><a class="close-link"></a></div>        <img src="#{smooshPath}" width="56" height="56" alt="#{prodName}" class="thumb" id="smoosh_img_#{id}"  style="background-color: #{hex};" />        <div class="popover-desc">            <span class="popover-title">#{prodName}</span>            <span attachpoint="inventoryStatusNode" class="inventory-status"></span>            <p>                <span attachpoint="swatchTitleNode"> </span>                 <span attachpoint="finishNameNode"> </span>            </p>                    <span class="popover-price">#{price}</span>        </div>        <div class="btn-container">            <input class="btn-remove hidden" type="image" src="/images/account/btn/btn_pop_remove_white_off#{imgSuffix}.gif" alt="Remove" name="btn_favorites_remove_#{id}" id="btn_favorites_remove_#{id}" value="" attachpoint="removeNode" />            <input class="btn-add" type="image" src="/images/popup/btn_add_to_bag#{imgSuffix}.gif" alt="#{addToBag}" name="prod_sku_#{id}" id="prod_sku_#{id}" value="" attachpoint="addToBagNode" />        </div>        <div class="popover-btm"></div>    </div></div>',
           cartConfirm: '<div id="#{id}" class="overlay-container cart-confirm-overlay-container">    <div class="overlay-content-popover popover-message">        <div class="close-container"><a class="close-link"></a></div>        <div attachPoint="cartConfirmDisplayNode">            <div class="popover-desc">                <span class="popover-title thank_you">#{text_thank_you}</span>                <p><span attachPoint="prodNameNode"></span><span attachPoint="shadeNameNode">&nbsp;- </span>&nbsp;<span attachPoint="addedMessageNode"></span></p>                <p class="cart-confirm-fss-message"></p>            </div>            <span class="popover-btn-checkout"><a href="/checkout/viewcart.tmpl"><img src="/images/popup/btn_checkout#{imgSuffix}.gif" alt="#{text_checkout}" class="btn-checkout"></a></span>            <span class="popover-btn-favorites"><a href="/account/favorites.tmpl"><img src="/images/popup/btn_favourites#{imgSuffix}.gif" alt="#{text_favorites}" class="btn-favorites" /></a></span>            <div class="close-link continue-link"><img src="/images/popup/btn_continue_shopping#{imgSuffix}.gif" alt="#{text_continue_shopping}" class="btn-continue"></div>        </div>        <div class="popover-desc hidden" attachPoint="cartConfirmErrorNode">             <span class="popover-title sorry">#{text_sorry}</span>            <p><span attachPoint="errorMessageNode"></span></p>              </div>        <div class="popover-btm"></div>    </div></div>',
           hexSwatch : '<div id="#{id}" class="swatch_hex_container" attachEvent="click:_onClick">    <div class="swatch_hex" attachPoint="hexNode" attachEvent="mouseover:_onMouseOver,mouseout:_onMouseOut" style="background-color: #{hex};"><br /></div>    <div class="tooltip" attachPoint="tooltipNode" style="background-color: #{hex};">#{name} #{inventory_status}</div></div>',
           hexSwatchImage : '<div id="#{id}" class="swatch_hex_container" attachPoint="shadeContainerNode" attachEvent="click:_onClick">    <div class="swatch_hex swatch_hex_smoosh" attachPoint="hexNode" attachEvent="mouseover:_onMouseOver,mouseout:_onMouseOut" style="background-color: #{hex};"><img src="#{smooshThumb}" width="12" height="12" alt=""></div>    <div class="tooltip" attachPoint="tooltipNode" style="background-color: #{hex};">#{name} #{inventory_status}</div></div>',
           thumbSwatch : '<div id="#{id}" class="swatch-thumb-container">    <a href="#" class="swatch-thumb"><img src="#{smooshThumb}" /><span class="tooltip" attachPoint="tooltipNode" style="background-color: #{hex};">#{text_select} #{name} #{text_toshop}</span></a></div>',
           thumbSwatchiPad :'<div id="#{id}" class="swatch-thumb-container">    <a href="#" class="swatch-thumb"><img src="#{smooshThumb}" /></a></div>',
           swatchCard : '<div id="#{id}" class="overlay-container swatchcard-container">    <div class="swatchcard">        <img id="smoosh-img-#{id}" class="swatch-lg" src="#{smooshPath}" alt="">        <div class="close-container"><a class="close-link"></a></div>        <div class="card-desc">            <p id="shade-name-#{id}" class="shade-name"> </p>            <p>                <span id="shade-description-#{id}"> </span>&nbsp;                <a href="javascript:void();" class="card-finish"><span id="shade-finish-swatchcards"> </span><span id="shade-finish-description-swatchcards" class="tooltip"> </span></a>            </p>            <div class="card-icons">                <a href="javascript:void(0);" id="limited-flag"><img src="/images/products/common/icon_limitedlife.gif" /><span class="tooltip">#{text_limited}</span></a>                <a href="javascript:void(0);" id="pro-flag"><img src="/images/products/common/icon_pro.gif"/><span class="tooltip">#{text_macpro}</span></a>            </div>            <p class="card-price">#{price}</p>        </div>        <div id="inventory-status-swatchcards" class="inventory-status"></div>        <input class="btn-add inventory-status-conditional" type="image" src="/images/products/btn/btn_add_to_bag_168#{imgSuffix}.gif" alt="" id="prod-sku-#{id}" value="#{skuPath}" />    </div></div>'
    },
    endeca : {
           contentResult : '<li class="#{css_class}">    <div class="productinfo">        <a href="#{link_url}"><img class="item" src="#{image}"/></a>        <div class="description">            <a href="#{link_url}"><strong class="itemname">#{header_text}</strong></a><br>            #{description}<br>            <a href="#{link_url}">#{link_text}</a>        </div>            </div></li>                    <li class="#{css_class}"><div class="righthr"></div></li>',
           dimensionsBreadcrumbLink : '<li>    <div class="navitem">        <a href="#" class="remove_filter" id="#{dimValueID}-link">    <div class="navtext">        #{Dim Value Name}            </div>        </a>    </div></li>',
           dimensionsNoRefinements : '<h3>No Filters Available.</h3>',
           dimensionsRefinementLink : '<li>        <div class="navitem">    <div class="navtext">        <a href="#" id="#{dimValueID}-link">#{Dim Value Name} (#{Number of Aggregate Records})</a>        </div>    </div></li>',
           dimensionsRefinementLinkShowAll : '<li>        <div class="navitem">    <div class="navtext">        <a href="#" id="#{dimValueID}-link">Show all »</a>        </div>    </div></li>',
           dimensionsRefinements : '<li class="collapsible" id="#{dimID}-header">    <a><h2 class="panelheader #{dimID}">#{Dimension Name}</h2></a>        <ul class="refinement_links" id="#{dimID}-content"></ul></li>',
           productResult : '<li id="#{SKU_ID}-prod">    <div class="productinfo">        <a href="#{url}"><img src="#{image}" class="item"></a>        <a href="#{url}"><strong class="itemname">#{PROD_RGN_NAME}</strong></a>        <div class="itemcost">#{formattedPrice}</div>                <br/>#{SHADENAME}<br/>        <a class="productlink" href="#{url}">#{linkText}</a><br/>        <div style="display:#{RATING_DISPLAY}" class="reviews">            <img src="/images/search/title_overallrating-blk.gif" class="overall_rating"/>            <img src="#{RATING_IMAGE}" class="rating"/>            ( <strong>#{AVERAGE_RATING_ROUNDED}</strong> out of <strong>#{RATING_RANGE}</strong> )        </div>        <div id="#{SKU_ID}-inventory-status" class="inventory-status addtobag" style="display:none;"></div>        <div id="#{SKU_ID}-add_to_bag" class="addtobag" style="display:none;">            <input type="image" src="/images/products/btn/btn_add_to_bag_93.gif" alt="" name="#{SKU_ID}-prod_sku" id="#{SKU_ID}-prod_sku" value="#{CATEGORY_ID}#{PRODUCT_ID}#{SKU_ID}"></input>            <div id="#{SKU_ID}-progress_add_to_bag" class="progress hidden"><br /></div>            <div id="#{SKU_ID}-cart_confirm_placeholder"></div>        </div>    </div></li><li class="righthr"></li>',
           summaryNoResults : '<div id="message">    <p>No items found matching "#{searchTerm}".</p>    <p class="#{didYouMeanClass}">Did you mean #{didYouMean}?</p>    <p>We suggest you check for typos, broaden your search and enter the type of product you need.</p></div>',
           summaryNoTerm : '<div id="message">    <p>Please enter a keyword in the search box on the left side of this page and click SEARCH.</p></div>',
           summaryResults : '<div id="message">    <!--Your search for "#{searchTerm}" returned #{originalRecords} #{resultText}: <a href="#">19 products</a>, <a href="#">1 articles</a>.    <p class="#{searchCorrectedClass}">However, "#{searchCorrected}" returned #{totalRecords} #{resultText}: <a href="#">19 products</a>, <a href="#">1 articles</a></p>-->        <p>Your search for "#{searchTerm}" returned the following results.</p>    <p class="#{didYouMeanClass}">Did you mean #{didYouMean}?</p></div>',
           topContentBanner : '<div id="rightcallout">    <a href="#{link_url}"><img src="#{image}" class="item"/></a>    <div class="description">        <a href="#{link_url}"><img id="rightcallouttitle" src="#{header_image_url}" alt="#{header_text}"/></a><br>        #{description}<br>        <a href="#{link_url}">#{link_text}</a>    </div></div>'
    }
};
/*  Prototype JavaScript framework, version 1.6.0.3
 *  (c) 2005-2008 Sam Stephenson
 *
 *  Prototype is freely distributable under the terms of an MIT-style license.
 *  For details, see the Prototype web site: http://www.prototypejs.org/
 *
 *--------------------------------------------------------------------------*/

var Prototype = {
  Version: '1.6.0.3',

  Browser: {
    IE:     !!(window.attachEvent &&
      navigator.userAgent.indexOf('Opera') === -1),
    Opera:  navigator.userAgent.indexOf('Opera') > -1,
    WebKit: navigator.userAgent.indexOf('AppleWebKit/') > -1,
    Gecko:  navigator.userAgent.indexOf('Gecko') > -1 &&
      navigator.userAgent.indexOf('KHTML') === -1,
    MobileSafari: !!navigator.userAgent.match(/Apple.*Mobile.*Safari/)
  },

  BrowserFeatures: {
    XPath: !!document.evaluate,
    SelectorsAPI: !!document.querySelector,
    ElementExtensions: !!window.HTMLElement,
    SpecificElementExtensions:
      document.createElement('div')['__proto__'] &&
      document.createElement('div')['__proto__'] !==
        document.createElement('form')['__proto__']
  },

  ScriptFragment: '<script[^>]*>([\\S\\s]*?)<\/script>',
  JSONFilter: /^\/\*-secure-([\s\S]*)\*\/\s*$/,

  emptyFunction: function() { },
  K: function(x) { return x }
};

if (Prototype.Browser.MobileSafari)
  Prototype.BrowserFeatures.SpecificElementExtensions = false;


/* Based on Alex Arnell's inheritance implementation. */
var Class = {
  create: function() {
    var parent = null, properties = $A(arguments);
    if (Object.isFunction(properties[0]))
      parent = properties.shift();

    function klass() {
      this.initialize.apply(this, arguments);
    }

    Object.extend(klass, Class.Methods);
    klass.superclass = parent;
    klass.subclasses = [];

    if (parent) {
      var subclass = function() { };
      subclass.prototype = parent.prototype;
      klass.prototype = new subclass;
      parent.subclasses.push(klass);
    }

    for (var i = 0; i < properties.length; i++)
      klass.addMethods(properties[i]);

    if (!klass.prototype.initialize)
      klass.prototype.initialize = Prototype.emptyFunction;

    klass.prototype.constructor = klass;

    return klass;
  }
};

Class.Methods = {
  addMethods: function(source) {
    var ancestor   = this.superclass && this.superclass.prototype;
    var properties = Object.keys(source);

    if (!Object.keys({ toString: true }).length)
      properties.push("toString", "valueOf");

    for (var i = 0, length = properties.length; i < length; i++) {
      var property = properties[i], value = source[property];
      if (ancestor && Object.isFunction(value) &&
          value.argumentNames().first() == "$super") {
        var method = value;
        value = (function(m) {
          return function() { return ancestor[m].apply(this, arguments) };
        })(property).wrap(method);

        value.valueOf = method.valueOf.bind(method);
        value.toString = method.toString.bind(method);
      }
      this.prototype[property] = value;
    }

    return this;
  }
};

var Abstract = { };

Object.extend = function(destination, source) {
  for (var property in source)
    destination[property] = source[property];
  return destination;
};

Object.extend(Object, {
  inspect: function(object) {
    try {
      if (Object.isUndefined(object)) return 'undefined';
      if (object === null) return 'null';
      return object.inspect ? object.inspect() : String(object);
    } catch (e) {
      if (e instanceof RangeError) return '...';
      throw e;
    }
  },

  toJSON: function(object) {
    var type = typeof object;
    switch (type) {
      case 'undefined':
      case 'function':
      case 'unknown': return;
      case 'boolean': return object.toString();
    }

    if (object === null) return 'null';
    if (object.toJSON) return object.toJSON();
    if (Object.isElement(object)) return;

    var results = [];
    for (var property in object) {
      var value = Object.toJSON(object[property]);
      if (!Object.isUndefined(value))
        results.push(property.toJSON() + ': ' + value);
    }

    return '{' + results.join(', ') + '}';
  },

  toQueryString: function(object) {
    return $H(object).toQueryString();
  },

  toHTML: function(object) {
    return object && object.toHTML ? object.toHTML() : String.interpret(object);
  },

  keys: function(object) {
    var keys = [];
    for (var property in object)
      keys.push(property);
    return keys;
  },

  values: function(object) {
    var values = [];
    for (var property in object)
      values.push(object[property]);
    return values;
  },

  clone: function(object) {
    return Object.extend({ }, object);
  },

  isElement: function(object) {
    return !!(object && object.nodeType == 1);
  },

  isArray: function(object) {
    return object != null && typeof object == "object" &&
      'splice' in object && 'join' in object;
  },

  isHash: function(object) {
    return object instanceof Hash;
  },

  isFunction: function(object) {
    return typeof object == "function";
  },

  isString: function(object) {
    return typeof object == "string";
  },

  isNumber: function(object) {
    return typeof object == "number";
  },

  isUndefined: function(object) {
    return typeof object == "undefined";
  }
});

Object.extend(Function.prototype, {
  argumentNames: function() {
    var names = this.toString().match(/^[\s\(]*function[^(]*\(([^\)]*)\)/)[1]
      .replace(/\s+/g, '').split(',');
    return names.length == 1 && !names[0] ? [] : names;
  },

  bind: function() {
    if (arguments.length < 2 && Object.isUndefined(arguments[0])) return this;
    var __method = this, args = $A(arguments), object = args.shift();
    return function() {
      return __method.apply(object, args.concat($A(arguments)));
    }
  },

  bindAsEventListener: function() {
    var __method = this, args = $A(arguments), object = args.shift();
    return function(event) {
      return __method.apply(object, [event || window.event].concat(args));
    }
  },

  curry: function() {
    if (!arguments.length) return this;
    var __method = this, args = $A(arguments);
    return function() {
      return __method.apply(this, args.concat($A(arguments)));
    }
  },

  delay: function() {
    var __method = this, args = $A(arguments), timeout = args.shift() * 1000;
    return window.setTimeout(function() {
      return __method.apply(__method, args);
    }, timeout);
  },

  defer: function() {
    var args = [0.01].concat($A(arguments));
    return this.delay.apply(this, args);
  },

  wrap: function(wrapper) {
    var __method = this;
    return function() {
      return wrapper.apply(this, [__method.bind(this)].concat($A(arguments)));
    }
  },

  methodize: function() {
    if (this._methodized) return this._methodized;
    var __method = this;
    return this._methodized = function() {
      return __method.apply(null, [this].concat($A(arguments)));
    };
  }
});

Date.prototype.toJSON = function() {
  return '"' + this.getUTCFullYear() + '-' +
    (this.getUTCMonth() + 1).toPaddedString(2) + '-' +
    this.getUTCDate().toPaddedString(2) + 'T' +
    this.getUTCHours().toPaddedString(2) + ':' +
    this.getUTCMinutes().toPaddedString(2) + ':' +
    this.getUTCSeconds().toPaddedString(2) + 'Z"';
};

var Try = {
  these: function() {
    var returnValue;

    for (var i = 0, length = arguments.length; i < length; i++) {
      var lambda = arguments[i];
      try {
        returnValue = lambda();
        break;
      } catch (e) { }
    }

    return returnValue;
  }
};

RegExp.prototype.match = RegExp.prototype.test;

RegExp.escape = function(str) {
  return String(str).replace(/([.*+?^=!:${}()|[\]\/\\])/g, '\\$1');
};

/*--------------------------------------------------------------------------*/

var PeriodicalExecuter = Class.create({
  initialize: function(callback, frequency) {
    this.callback = callback;
    this.frequency = frequency;
    this.currentlyExecuting = false;

    this.registerCallback();
  },

  registerCallback: function() {
    this.timer = setInterval(this.onTimerEvent.bind(this), this.frequency * 1000);
  },

  execute: function() {
    this.callback(this);
  },

  stop: function() {
    if (!this.timer) return;
    clearInterval(this.timer);
    this.timer = null;
  },

  onTimerEvent: function() {
    if (!this.currentlyExecuting) {
      try {
        this.currentlyExecuting = true;
        this.execute();
      } finally {
        this.currentlyExecuting = false;
      }
    }
  }
});
Object.extend(String, {
  interpret: function(value) {
    return value == null ? '' : String(value);
  },
  specialChar: {
    '\b': '\\b',
    '\t': '\\t',
    '\n': '\\n',
    '\f': '\\f',
    '\r': '\\r',
    '\\': '\\\\'
  }
});

Object.extend(String.prototype, {
  gsub: function(pattern, replacement) {
    var result = '', source = this, match;
    replacement = arguments.callee.prepareReplacement(replacement);

    while (source.length > 0) {
      if (match = source.match(pattern)) {
        result += source.slice(0, match.index);
        result += String.interpret(replacement(match));
        source  = source.slice(match.index + match[0].length);
      } else {
        result += source, source = '';
      }
    }
    return result;
  },

  sub: function(pattern, replacement, count) {
    replacement = this.gsub.prepareReplacement(replacement);
    count = Object.isUndefined(count) ? 1 : count;

    return this.gsub(pattern, function(match) {
      if (--count < 0) return match[0];
      return replacement(match);
    });
  },

  scan: function(pattern, iterator) {
    this.gsub(pattern, iterator);
    return String(this);
  },

  truncate: function(length, truncation) {
    length = length || 30;
    truncation = Object.isUndefined(truncation) ? '...' : truncation;
    return this.length > length ?
      this.slice(0, length - truncation.length) + truncation : String(this);
  },

  strip: function() {
    return this.replace(/^\s+/, '').replace(/\s+$/, '');
  },

  stripTags: function() {
    return this.replace(/<\/?[^>]+>/gi, '');
  },

  stripScripts: function() {
    return this.replace(new RegExp(Prototype.ScriptFragment, 'img'), '');
  },

  extractScripts: function() {
    var matchAll = new RegExp(Prototype.ScriptFragment, 'img');
    var matchOne = new RegExp(Prototype.ScriptFragment, 'im');
    return (this.match(matchAll) || []).map(function(scriptTag) {
      return (scriptTag.match(matchOne) || ['', ''])[1];
    });
  },

  evalScripts: function() {
    return this.extractScripts().map(function(script) { return eval(script) });
  },

  escapeHTML: function() {
    var self = arguments.callee;
    self.text.data = this;
    return self.div.innerHTML;
  },

  unescapeHTML: function() {
    var div = new Element('div');
    div.innerHTML = this.stripTags();
    return div.childNodes[0] ? (div.childNodes.length > 1 ?
      $A(div.childNodes).inject('', function(memo, node) { return memo+node.nodeValue }) :
      div.childNodes[0].nodeValue) : '';
  },

  toQueryParams: function(separator) {
    var match = this.strip().match(/([^?#]*)(#.*)?$/);
    if (!match) return { };

    return match[1].split(separator || '&').inject({ }, function(hash, pair) {
      if ((pair = pair.split('='))[0]) {
        var key = decodeURIComponent(pair.shift());
        var value = pair.length > 1 ? pair.join('=') : pair[0];
        if (value != undefined) value = decodeURIComponent(value);

        if (key in hash) {
          if (!Object.isArray(hash[key])) hash[key] = [hash[key]];
          hash[key].push(value);
        }
        else hash[key] = value;
      }
      return hash;
    });
  },

  toArray: function() {
    return this.split('');
  },

  succ: function() {
    return this.slice(0, this.length - 1) +
      String.fromCharCode(this.charCodeAt(this.length - 1) + 1);
  },

  times: function(count) {
    return count < 1 ? '' : new Array(count + 1).join(this);
  },

  camelize: function() {
    var parts = this.split('-'), len = parts.length;
    if (len == 1) return parts[0];

    var camelized = this.charAt(0) == '-'
      ? parts[0].charAt(0).toUpperCase() + parts[0].substring(1)
      : parts[0];

    for (var i = 1; i < len; i++)
      camelized += parts[i].charAt(0).toUpperCase() + parts[i].substring(1);

    return camelized;
  },

  capitalize: function() {
    return this.charAt(0).toUpperCase() + this.substring(1).toLowerCase();
  },

  underscore: function() {
    return this.gsub(/::/, '/').gsub(/([A-Z]+)([A-Z][a-z])/,'#{1}_#{2}').gsub(/([a-z\d])([A-Z])/,'#{1}_#{2}').gsub(/-/,'_').toLowerCase();
  },

  dasherize: function() {
    return this.gsub(/_/,'-');
  },

  inspect: function(useDoubleQuotes) {
    var escapedString = this.gsub(/[\x00-\x1f\\]/, function(match) {
      var character = String.specialChar[match[0]];
      return character ? character : '\\u00' + match[0].charCodeAt().toPaddedString(2, 16);
    });
    if (useDoubleQuotes) return '"' + escapedString.replace(/"/g, '\\"') + '"';
    return "'" + escapedString.replace(/'/g, '\\\'') + "'";
  },

  toJSON: function() {
    return this.inspect(true);
  },

  unfilterJSON: function(filter) {
    return this.sub(filter || Prototype.JSONFilter, '#{1}');
  },

  isJSON: function() {
    var str = this;
    if (str.blank()) return false;
    str = this.replace(/\\./g, '@').replace(/"[^"\\\n\r]*"/g, '');
    return (/^[,:{}\[\]0-9.\-+Eaeflnr-u \n\r\t]*$/).test(str);
  },

  evalJSON: function(sanitize) {
    var json = this.unfilterJSON();
    try {
      if (!sanitize || json.isJSON()) return eval('(' + json + ')');
    } catch (e) { }
    throw new SyntaxError('Badly formed JSON string: ' + this.inspect());
  },

  include: function(pattern) {
    return this.indexOf(pattern) > -1;
  },

  startsWith: function(pattern) {
    return this.indexOf(pattern) === 0;
  },

  endsWith: function(pattern) {
    var d = this.length - pattern.length;
    return d >= 0 && this.lastIndexOf(pattern) === d;
  },

  empty: function() {
    return this == '';
  },

  blank: function() {
    return /^\s*$/.test(this);
  },

  interpolate: function(object, pattern) {
    return new Template(this, pattern).evaluate(object);
  }
});

if (Prototype.Browser.WebKit || Prototype.Browser.IE) Object.extend(String.prototype, {
  escapeHTML: function() {
    return this.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');
  },
  unescapeHTML: function() {
    return this.stripTags().replace(/&amp;/g,'&').replace(/&lt;/g,'<').replace(/&gt;/g,'>');
  }
});

String.prototype.gsub.prepareReplacement = function(replacement) {
  if (Object.isFunction(replacement)) return replacement;
  var template = new Template(replacement);
  return function(match) { return template.evaluate(match) };
};

String.prototype.parseQuery = String.prototype.toQueryParams;

Object.extend(String.prototype.escapeHTML, {
  div:  document.createElement('div'),
  text: document.createTextNode('')
});

String.prototype.escapeHTML.div.appendChild(String.prototype.escapeHTML.text);

var Template = Class.create({
  initialize: function(template, pattern) {
    this.template = template.toString();
    this.pattern = pattern || Template.Pattern;
  },

  evaluate: function(object) {
    if (Object.isFunction(object.toTemplateReplacements))
      object = object.toTemplateReplacements();

    return this.template.gsub(this.pattern, function(match) {
      if (object == null) return '';

      var before = match[1] || '';
      if (before == '\\') return match[2];

      var ctx = object, expr = match[3];
      var pattern = /^([^.[]+|\[((?:.*?[^\\])?)\])(\.|\[|$)/;
      match = pattern.exec(expr);
      if (match == null) return before;

      while (match != null) {
        var comp = match[1].startsWith('[') ? match[2].gsub('\\\\]', ']') : match[1];
        ctx = ctx[comp];
        if (null == ctx || '' == match[3]) break;
        expr = expr.substring('[' == match[3] ? match[1].length : match[0].length);
        match = pattern.exec(expr);
      }

      return before + String.interpret(ctx);
    });
  }
});
Template.Pattern = /(^|.|\r|\n)(#\{(.*?)\})/;

var $break = { };

var Enumerable = {
  each: function(iterator, context) {
    var index = 0;
    try {
      this._each(function(value) {
        iterator.call(context, value, index++);
      });
    } catch (e) {
      if (e != $break) throw e;
    }
    return this;
  },

  eachSlice: function(number, iterator, context) {
    var index = -number, slices = [], array = this.toArray();
    if (number < 1) return array;
    while ((index += number) < array.length)
      slices.push(array.slice(index, index+number));
    return slices.collect(iterator, context);
  },

  all: function(iterator, context) {
    iterator = iterator || Prototype.K;
    var result = true;
    this.each(function(value, index) {
      result = result && !!iterator.call(context, value, index);
      if (!result) throw $break;
    });
    return result;
  },

  any: function(iterator, context) {
    iterator = iterator || Prototype.K;
    var result = false;
    this.each(function(value, index) {
      if (result = !!iterator.call(context, value, index))
        throw $break;
    });
    return result;
  },

  collect: function(iterator, context) {
    iterator = iterator || Prototype.K;
    var results = [];
    this.each(function(value, index) {
      results.push(iterator.call(context, value, index));
    });
    return results;
  },

  detect: function(iterator, context) {
    var result;
    this.each(function(value, index) {
      if (iterator.call(context, value, index)) {
        result = value;
        throw $break;
      }
    });
    return result;
  },

  findAll: function(iterator, context) {
    var results = [];
    this.each(function(value, index) {
      if (iterator.call(context, value, index))
        results.push(value);
    });
    return results;
  },

  grep: function(filter, iterator, context) {
    iterator = iterator || Prototype.K;
    var results = [];

    if (Object.isString(filter))
      filter = new RegExp(filter);

    this.each(function(value, index) {
      if (filter.match(value))
        results.push(iterator.call(context, value, index));
    });
    return results;
  },

  include: function(object) {
    if (Object.isFunction(this.indexOf))
      if (this.indexOf(object) != -1) return true;

    var found = false;
    this.each(function(value) {
      if (value == object) {
        found = true;
        throw $break;
      }
    });
    return found;
  },

  inGroupsOf: function(number, fillWith) {
    fillWith = Object.isUndefined(fillWith) ? null : fillWith;
    return this.eachSlice(number, function(slice) {
      while(slice.length < number) slice.push(fillWith);
      return slice;
    });
  },

  inject: function(memo, iterator, context) {
    this.each(function(value, index) {
      memo = iterator.call(context, memo, value, index);
    });
    return memo;
  },

  invoke: function(method) {
    var args = $A(arguments).slice(1);
    return this.map(function(value) {
      return value[method].apply(value, args);
    });
  },

  max: function(iterator, context) {
    iterator = iterator || Prototype.K;
    var result;
    this.each(function(value, index) {
      value = iterator.call(context, value, index);
      if (result == null || value >= result)
        result = value;
    });
    return result;
  },

  min: function(iterator, context) {
    iterator = iterator || Prototype.K;
    var result;
    this.each(function(value, index) {
      value = iterator.call(context, value, index);
      if (result == null || value < result)
        result = value;
    });
    return result;
  },

  partition: function(iterator, context) {
    iterator = iterator || Prototype.K;
    var trues = [], falses = [];
    this.each(function(value, index) {
      (iterator.call(context, value, index) ?
        trues : falses).push(value);
    });
    return [trues, falses];
  },

  pluck: function(property) {
    var results = [];
    this.each(function(value) {
      results.push(value[property]);
    });
    return results;
  },

  reject: function(iterator, context) {
    var results = [];
    this.each(function(value, index) {
      if (!iterator.call(context, value, index))
        results.push(value);
    });
    return results;
  },

  sortBy: function(iterator, context) {
    return this.map(function(value, index) {
      return {
        value: value,
        criteria: iterator.call(context, value, index)
      };
    }).sort(function(left, right) {
      var a = left.criteria, b = right.criteria;
      return a < b ? -1 : a > b ? 1 : 0;
    }).pluck('value');
  },

  toArray: function() {
    return this.map();
  },

  zip: function() {
    var iterator = Prototype.K, args = $A(arguments);
    if (Object.isFunction(args.last()))
      iterator = args.pop();

    var collections = [this].concat(args).map($A);
    return this.map(function(value, index) {
      return iterator(collections.pluck(index));
    });
  },

  size: function() {
    return this.toArray().length;
  },

  inspect: function() {
    return '#<Enumerable:' + this.toArray().inspect() + '>';
  }
};

Object.extend(Enumerable, {
  map:     Enumerable.collect,
  find:    Enumerable.detect,
  select:  Enumerable.findAll,
  filter:  Enumerable.findAll,
  member:  Enumerable.include,
  entries: Enumerable.toArray,
  every:   Enumerable.all,
  some:    Enumerable.any
});
function $A(iterable) {
  if (!iterable) return [];
  if (iterable.toArray) return iterable.toArray();
  var length = iterable.length || 0, results = new Array(length);
  while (length--) results[length] = iterable[length];
  return results;
}

if (Prototype.Browser.WebKit) {
  $A = function(iterable) {
    if (!iterable) return [];
    // In Safari, only use the `toArray` method if it's not a NodeList.
    // A NodeList is a function, has an function `item` property, and a numeric
    // `length` property. Adapted from Google Doctype.
    if (!(typeof iterable === 'function' && typeof iterable.length ===
        'number' && typeof iterable.item === 'function') && iterable.toArray)
      return iterable.toArray();
    var length = iterable.length || 0, results = new Array(length);
    while (length--) results[length] = iterable[length];
    return results;
  };
}

Array.from = $A;

Object.extend(Array.prototype, Enumerable);

if (!Array.prototype._reverse) Array.prototype._reverse = Array.prototype.reverse;

Object.extend(Array.prototype, {
  _each: function(iterator) {
    for (var i = 0, length = this.length; i < length; i++)
      iterator(this[i]);
  },

  clear: function() {
    this.length = 0;
    return this;
  },

  first: function() {
    return this[0];
  },

  last: function() {
    return this[this.length - 1];
  },

  compact: function() {
    return this.select(function(value) {
      return value != null;
    });
  },

  flatten: function() {
    return this.inject([], function(array, value) {
      return array.concat(Object.isArray(value) ?
        value.flatten() : [value]);
    });
  },

  without: function() {
    var values = $A(arguments);
    return this.select(function(value) {
      return !values.include(value);
    });
  },

  reverse: function(inline) {
    return (inline !== false ? this : this.toArray())._reverse();
  },

  reduce: function() {
    return this.length > 1 ? this : this[0];
  },

  uniq: function(sorted) {
    return this.inject([], function(array, value, index) {
      if (0 == index || (sorted ? array.last() != value : !array.include(value)))
        array.push(value);
      return array;
    });
  },

  intersect: function(array) {
    return this.uniq().findAll(function(item) {
      return array.detect(function(value) { return item === value });
    });
  },

  clone: function() {
    return [].concat(this);
  },

  size: function() {
    return this.length;
  },

  inspect: function() {
    return '[' + this.map(Object.inspect).join(', ') + ']';
  },

  toJSON: function() {
    var results = [];
    this.each(function(object) {
      var value = Object.toJSON(object);
      if (!Object.isUndefined(value)) results.push(value);
    });
    return '[' + results.join(', ') + ']';
  }
});

// use native browser JS 1.6 implementation if available
if (Object.isFunction(Array.prototype.forEach))
  Array.prototype._each = Array.prototype.forEach;

if (!Array.prototype.indexOf) Array.prototype.indexOf = function(item, i) {
  i || (i = 0);
  var length = this.length;
  if (i < 0) i = length + i;
  for (; i < length; i++)
    if (this[i] === item) return i;
  return -1;
};

if (!Array.prototype.lastIndexOf) Array.prototype.lastIndexOf = function(item, i) {
  i = isNaN(i) ? this.length : (i < 0 ? this.length + i : i) + 1;
  var n = this.slice(0, i).reverse().indexOf(item);
  return (n < 0) ? n : i - n - 1;
};

Array.prototype.toArray = Array.prototype.clone;

function $w(string) {
  if (!Object.isString(string)) return [];
  string = string.strip();
  return string ? string.split(/\s+/) : [];
}

if (Prototype.Browser.Opera){
  Array.prototype.concat = function() {
    var array = [];
    for (var i = 0, length = this.length; i < length; i++) array.push(this[i]);
    for (var i = 0, length = arguments.length; i < length; i++) {
      if (Object.isArray(arguments[i])) {
        for (var j = 0, arrayLength = arguments[i].length; j < arrayLength; j++)
          array.push(arguments[i][j]);
      } else {
        array.push(arguments[i]);
      }
    }
    return array;
  };
}
Object.extend(Number.prototype, {
  toColorPart: function() {
    return this.toPaddedString(2, 16);
  },

  succ: function() {
    return this + 1;
  },

  times: function(iterator, context) {
    $R(0, this, true).each(iterator, context);
    return this;
  },

  toPaddedString: function(length, radix) {
    var string = this.toString(radix || 10);
    return '0'.times(length - string.length) + string;
  },

  toJSON: function() {
    return isFinite(this) ? this.toString() : 'null';
  }
});

$w('abs round ceil floor').each(function(method){
  Number.prototype[method] = Math[method].methodize();
});
function $H(object) {
  return new Hash(object);
};

var Hash = Class.create(Enumerable, (function() {

  function toQueryPair(key, value) {
    if (Object.isUndefined(value)) return key;
    return key + '=' + encodeURIComponent(String.interpret(value));
  }

  return {
    initialize: function(object) {
      this._object = Object.isHash(object) ? object.toObject() : Object.clone(object);
    },

    _each: function(iterator) {
      for (var key in this._object) {
        var value = this._object[key], pair = [key, value];
        pair.key = key;
        pair.value = value;
        iterator(pair);
      }
    },

    set: function(key, value) {
      return this._object[key] = value;
    },

    get: function(key) {
      // simulating poorly supported hasOwnProperty
      if (this._object[key] !== Object.prototype[key])
        return this._object[key];
    },

    unset: function(key) {
      var value = this._object[key];
      delete this._object[key];
      return value;
    },

    toObject: function() {
      return Object.clone(this._object);
    },

    keys: function() {
      return this.pluck('key');
    },

    values: function() {
      return this.pluck('value');
    },

    index: function(value) {
      var match = this.detect(function(pair) {
        return pair.value === value;
      });
      return match && match.key;
    },

    merge: function(object) {
      return this.clone().update(object);
    },

    update: function(object) {
      return new Hash(object).inject(this, function(result, pair) {
        result.set(pair.key, pair.value);
        return result;
      });
    },

    toQueryString: function() {
      return this.inject([], function(results, pair) {
        var key = encodeURIComponent(pair.key), values = pair.value;

        if (values && typeof values == 'object') {
          if (Object.isArray(values))
            return results.concat(values.map(toQueryPair.curry(key)));
        } else results.push(toQueryPair(key, values));
        return results;
      }).join('&');
    },

    inspect: function() {
      return '#<Hash:{' + this.map(function(pair) {
        return pair.map(Object.inspect).join(': ');
      }).join(', ') + '}>';
    },

    toJSON: function() {
      return Object.toJSON(this.toObject());
    },

    clone: function() {
      return new Hash(this);
    }
  }
})());

Hash.prototype.toTemplateReplacements = Hash.prototype.toObject;
Hash.from = $H;
var ObjectRange = Class.create(Enumerable, {
  initialize: function(start, end, exclusive) {
    this.start = start;
    this.end = end;
    this.exclusive = exclusive;
  },

  _each: function(iterator) {
    var value = this.start;
    while (this.include(value)) {
      iterator(value);
      value = value.succ();
    }
  },

  include: function(value) {
    if (value < this.start)
      return false;
    if (this.exclusive)
      return value < this.end;
    return value <= this.end;
  }
});

var $R = function(start, end, exclusive) {
  return new ObjectRange(start, end, exclusive);
};

var Ajax = {
  getTransport: function() {
    return Try.these(
      function() {return new XMLHttpRequest()},
      function() {return new ActiveXObject('Msxml2.XMLHTTP')},
      function() {return new ActiveXObject('Microsoft.XMLHTTP')}
    ) || false;
  },

  activeRequestCount: 0
};

Ajax.Responders = {
  responders: [],

  _each: function(iterator) {
    this.responders._each(iterator);
  },

  register: function(responder) {
    if (!this.include(responder))
      this.responders.push(responder);
  },

  unregister: function(responder) {
    this.responders = this.responders.without(responder);
  },

  dispatch: function(callback, request, transport, json) {
    this.each(function(responder) {
      if (Object.isFunction(responder[callback])) {
        try {
          responder[callback].apply(responder, [request, transport, json]);
        } catch (e) { }
      }
    });
  }
};

Object.extend(Ajax.Responders, Enumerable);

Ajax.Responders.register({
  onCreate:   function() { Ajax.activeRequestCount++ },
  onComplete: function() { Ajax.activeRequestCount-- }
});

Ajax.Base = Class.create({
  initialize: function(options) {
    this.options = {
      method:       'post',
      asynchronous: true,
      contentType:  'application/x-www-form-urlencoded',
      encoding:     'UTF-8',
      parameters:   '',
      evalJSON:     true,
      evalJS:       true
    };
    Object.extend(this.options, options || { });

    this.options.method = this.options.method.toLowerCase();

    if (Object.isString(this.options.parameters))
      this.options.parameters = this.options.parameters.toQueryParams();
    else if (Object.isHash(this.options.parameters))
      this.options.parameters = this.options.parameters.toObject();
  }
});

Ajax.Request = Class.create(Ajax.Base, {
  _complete: false,

  initialize: function($super, url, options) {
    $super(options);
    this.transport = Ajax.getTransport();
    this.request(url);
  },

  request: function(url) {
    this.url = url;
    this.method = this.options.method;
    var params = Object.clone(this.options.parameters);

    if (!['get', 'post'].include(this.method)) {
      // simulate other verbs over post
      params['_method'] = this.method;
      this.method = 'post';
    }

    this.parameters = params;

    if (params = Object.toQueryString(params)) {
      // when GET, append parameters to URL
      if (this.method == 'get')
        this.url += (this.url.include('?') ? '&' : '?') + params;
      else if (/Konqueror|Safari|KHTML/.test(navigator.userAgent))
        params += '&_=';
    }

    try {
      var response = new Ajax.Response(this);
      if (this.options.onCreate) this.options.onCreate(response);
      Ajax.Responders.dispatch('onCreate', this, response);

      this.transport.open(this.method.toUpperCase(), this.url,
        this.options.asynchronous);

      if (this.options.asynchronous) this.respondToReadyState.bind(this).defer(1);

      this.transport.onreadystatechange = this.onStateChange.bind(this);
      this.setRequestHeaders();

      this.body = this.method == 'post' ? (this.options.postBody || params) : null;
      this.transport.send(this.body);

      /* Force Firefox to handle ready state 4 for synchronous requests */
      if (!this.options.asynchronous && this.transport.overrideMimeType)
        this.onStateChange();

    }
    catch (e) {
      this.dispatchException(e);
    }
  },

  onStateChange: function() {
    var readyState = this.transport.readyState;
    if (readyState > 1 && !((readyState == 4) && this._complete))
      this.respondToReadyState(this.transport.readyState);
  },

  setRequestHeaders: function() {
    var headers = {
      'X-Requested-With': 'XMLHttpRequest',
      'X-Prototype-Version': Prototype.Version,
      'Accept': 'text/javascript, text/html, application/xml, text/xml, */*'
    };

    if (this.method == 'post') {
      headers['Content-type'] = this.options.contentType +
        (this.options.encoding ? '; charset=' + this.options.encoding : '');

      /* Force "Connection: close" for older Mozilla browsers to work
       * around a bug where XMLHttpRequest sends an incorrect
       * Content-length header. See Mozilla Bugzilla #246651.
       */
      if (this.transport.overrideMimeType &&
          (navigator.userAgent.match(/Gecko\/(\d{4})/) || [0,2005])[1] < 2005)
            headers['Connection'] = 'close';
    }

    // user-defined headers
    if (typeof this.options.requestHeaders == 'object') {
      var extras = this.options.requestHeaders;

      if (Object.isFunction(extras.push))
        for (var i = 0, length = extras.length; i < length; i += 2)
          headers[extras[i]] = extras[i+1];
      else
        $H(extras).each(function(pair) { headers[pair.key] = pair.value });
    }

    for (var name in headers)
      this.transport.setRequestHeader(name, headers[name]);
  },

  success: function() {
    var status = this.getStatus();
    return !status || (status >= 200 && status < 300);
  },

  getStatus: function() {
    try {
      return this.transport.status || 0;
    } catch (e) { return 0 }
  },

  respondToReadyState: function(readyState) {
    var state = Ajax.Request.Events[readyState], response = new Ajax.Response(this);

    if (state == 'Complete') {
      try {
        this._complete = true;
        (this.options['on' + response.status]
         || this.options['on' + (this.success() ? 'Success' : 'Failure')]
         || Prototype.emptyFunction)(response, response.headerJSON);
      } catch (e) {
        this.dispatchException(e);
      }

      var contentType = response.getHeader('Content-type');
      if (this.options.evalJS == 'force'
          || (this.options.evalJS && this.isSameOrigin() && contentType
          && contentType.match(/^\s*(text|application)\/(x-)?(java|ecma)script(;.*)?\s*$/i)))
        this.evalResponse();
    }

    try {
      (this.options['on' + state] || Prototype.emptyFunction)(response, response.headerJSON);
      Ajax.Responders.dispatch('on' + state, this, response, response.headerJSON);
    } catch (e) {
      this.dispatchException(e);
    }

    if (state == 'Complete') {
      // avoid memory leak in MSIE: clean up
      this.transport.onreadystatechange = Prototype.emptyFunction;
    }
  },

  isSameOrigin: function() {
    var m = this.url.match(/^\s*https?:\/\/[^\/]*/);
    return !m || (m[0] == '#{protocol}//#{domain}#{port}'.interpolate({
      protocol: location.protocol,
      domain: document.domain,
      port: location.port ? ':' + location.port : ''
    }));
  },

  getHeader: function(name) {
    try {
      return this.transport.getResponseHeader(name) || null;
    } catch (e) { return null }
  },

  evalResponse: function() {
    try {
      return eval((this.transport.responseText || '').unfilterJSON());
    } catch (e) {
      this.dispatchException(e);
    }
  },

  dispatchException: function(exception) {
    (this.options.onException || Prototype.emptyFunction)(this, exception);
    Ajax.Responders.dispatch('onException', this, exception);
  }
});

Ajax.Request.Events =
  ['Uninitialized', 'Loading', 'Loaded', 'Interactive', 'Complete'];

Ajax.Response = Class.create({
  initialize: function(request){
    this.request = request;
    var transport  = this.transport  = request.transport,
        readyState = this.readyState = transport.readyState;

    if((readyState > 2 && !Prototype.Browser.IE) || readyState == 4) {
      this.status       = this.getStatus();
      this.statusText   = this.getStatusText();
      this.responseText = String.interpret(transport.responseText);
      this.headerJSON   = this._getHeaderJSON();
    }

    if(readyState == 4) {
      var xml = transport.responseXML;
      this.responseXML  = Object.isUndefined(xml) ? null : xml;
      this.responseJSON = this._getResponseJSON();
    }
  },

  status:      0,
  statusText: '',

  getStatus: Ajax.Request.prototype.getStatus,

  getStatusText: function() {
    try {
      return this.transport.statusText || '';
    } catch (e) { return '' }
  },

  getHeader: Ajax.Request.prototype.getHeader,

  getAllHeaders: function() {
    try {
      return this.getAllResponseHeaders();
    } catch (e) { return null }
  },

  getResponseHeader: function(name) {
    return this.transport.getResponseHeader(name);
  },

  getAllResponseHeaders: function() {
    return this.transport.getAllResponseHeaders();
  },

  _getHeaderJSON: function() {
    var json = this.getHeader('X-JSON');
    if (!json) return null;
    json = decodeURIComponent(escape(json));
    try {
      return json.evalJSON(this.request.options.sanitizeJSON ||
        !this.request.isSameOrigin());
    } catch (e) {
      this.request.dispatchException(e);
    }
  },

  _getResponseJSON: function() {
    var options = this.request.options;
    if (!options.evalJSON || (options.evalJSON != 'force' &&
      !(this.getHeader('Content-type') || '').include('application/json')) ||
        this.responseText.blank())
          return null;
    try {
      return this.responseText.evalJSON(options.sanitizeJSON ||
        !this.request.isSameOrigin());
    } catch (e) {
      this.request.dispatchException(e);
    }
  }
});

Ajax.Updater = Class.create(Ajax.Request, {
  initialize: function($super, container, url, options) {
    this.container = {
      success: (container.success || container),
      failure: (container.failure || (container.success ? null : container))
    };

    options = Object.clone(options);
    var onComplete = options.onComplete;
    options.onComplete = (function(response, json) {
      this.updateContent(response.responseText);
      if (Object.isFunction(onComplete)) onComplete(response, json);
    }).bind(this);

    $super(url, options);
  },

  updateContent: function(responseText) {
    var receiver = this.container[this.success() ? 'success' : 'failure'],
        options = this.options;

    if (!options.evalScripts) responseText = responseText.stripScripts();

    if (receiver = $(receiver)) {
      if (options.insertion) {
        if (Object.isString(options.insertion)) {
          var insertion = { }; insertion[options.insertion] = responseText;
          receiver.insert(insertion);
        }
        else options.insertion(receiver, responseText);
      }
      else receiver.update(responseText);
    }
  }
});

Ajax.PeriodicalUpdater = Class.create(Ajax.Base, {
  initialize: function($super, container, url, options) {
    $super(options);
    this.onComplete = this.options.onComplete;

    this.frequency = (this.options.frequency || 2);
    this.decay = (this.options.decay || 1);

    this.updater = { };
    this.container = container;
    this.url = url;

    this.start();
  },

  start: function() {
    this.options.onComplete = this.updateComplete.bind(this);
    this.onTimerEvent();
  },

  stop: function() {
    this.updater.options.onComplete = undefined;
    clearTimeout(this.timer);
    (this.onComplete || Prototype.emptyFunction).apply(this, arguments);
  },

  updateComplete: function(response) {
    if (this.options.decay) {
      this.decay = (response.responseText == this.lastText ?
        this.decay * this.options.decay : 1);

      this.lastText = response.responseText;
    }
    this.timer = this.onTimerEvent.bind(this).delay(this.decay * this.frequency);
  },

  onTimerEvent: function() {
    this.updater = new Ajax.Updater(this.container, this.url, this.options);
  }
});
function $(element) {
  if (arguments.length > 1) {
    for (var i = 0, elements = [], length = arguments.length; i < length; i++)
      elements.push($(arguments[i]));
    return elements;
  }
  if (Object.isString(element))
    element = document.getElementById(element);
  return Element.extend(element);
}

if (Prototype.BrowserFeatures.XPath) {
  document._getElementsByXPath = function(expression, parentElement) {
    var results = [];
    var query = document.evaluate(expression, $(parentElement) || document,
      null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null);
    for (var i = 0, length = query.snapshotLength; i < length; i++)
      results.push(Element.extend(query.snapshotItem(i)));
    return results;
  };
}

/*--------------------------------------------------------------------------*/

if (!window.Node) var Node = { };

if (!Node.ELEMENT_NODE) {
  // DOM level 2 ECMAScript Language Binding
  Object.extend(Node, {
    ELEMENT_NODE: 1,
    ATTRIBUTE_NODE: 2,
    TEXT_NODE: 3,
    CDATA_SECTION_NODE: 4,
    ENTITY_REFERENCE_NODE: 5,
    ENTITY_NODE: 6,
    PROCESSING_INSTRUCTION_NODE: 7,
    COMMENT_NODE: 8,
    DOCUMENT_NODE: 9,
    DOCUMENT_TYPE_NODE: 10,
    DOCUMENT_FRAGMENT_NODE: 11,
    NOTATION_NODE: 12
  });
}

(function() {
  var element = this.Element;
  this.Element = function(tagName, attributes) {
    attributes = attributes || { };
    tagName = tagName.toLowerCase();
    var cache = Element.cache;
    if (Prototype.Browser.IE && attributes.name) {
      tagName = '<' + tagName + ' name="' + attributes.name + '">';
      delete attributes.name;
      return Element.writeAttribute(document.createElement(tagName), attributes);
    }
    if (!cache[tagName]) cache[tagName] = Element.extend(document.createElement(tagName));
    return Element.writeAttribute(cache[tagName].cloneNode(false), attributes);
  };
  Object.extend(this.Element, element || { });
  if (element) this.Element.prototype = element.prototype;
}).call(window);

Element.cache = { };

Element.Methods = {
  visible: function(element) {
    return $(element).style.display != 'none';
  },

  toggle: function(element) {
    element = $(element);
    Element[Element.visible(element) ? 'hide' : 'show'](element);
    return element;
  },

  hide: function(element) {
    element = $(element);
    element.style.display = 'none';
    return element;
  },

  show: function(element) {
    element = $(element);
    element.style.display = '';
    return element;
  },

  remove: function(element) {
    element = $(element);
    element.parentNode.removeChild(element);
    return element;
  },

  update: function(element, content) {
    element = $(element);
    if (content && content.toElement) content = content.toElement();
    if (Object.isElement(content)) return element.update().insert(content);
    content = Object.toHTML(content);
    element.innerHTML = content.stripScripts();
    content.evalScripts.bind(content).defer();
    return element;
  },

  replace: function(element, content) {
    element = $(element);
    if (content && content.toElement) content = content.toElement();
    else if (!Object.isElement(content)) {
      content = Object.toHTML(content);
      var range = element.ownerDocument.createRange();
      range.selectNode(element);
      content.evalScripts.bind(content).defer();
      content = range.createContextualFragment(content.stripScripts());
    }
    element.parentNode.replaceChild(content, element);
    return element;
  },

  insert: function(element, insertions) {
    element = $(element);

    if (Object.isString(insertions) || Object.isNumber(insertions) ||
        Object.isElement(insertions) || (insertions && (insertions.toElement || insertions.toHTML)))
          insertions = {bottom:insertions};

    var content, insert, tagName, childNodes;

    for (var position in insertions) {
      content  = insertions[position];
      position = position.toLowerCase();
      insert = Element._insertionTranslations[position];

      if (content && content.toElement) content = content.toElement();
      if (Object.isElement(content)) {
        insert(element, content);
        continue;
      }

      content = Object.toHTML(content);

      tagName = ((position == 'before' || position == 'after')
        ? element.parentNode : element).tagName.toUpperCase();

      childNodes = Element._getContentFromAnonymousElement(tagName, content.stripScripts());

      if (position == 'top' || position == 'after') childNodes.reverse();
      childNodes.each(insert.curry(element));

      content.evalScripts.bind(content).defer();
    }

    return element;
  },

  wrap: function(element, wrapper, attributes) {
    element = $(element);
    if (Object.isElement(wrapper))
      $(wrapper).writeAttribute(attributes || { });
    else if (Object.isString(wrapper)) wrapper = new Element(wrapper, attributes);
    else wrapper = new Element('div', wrapper);
    if (element.parentNode)
      element.parentNode.replaceChild(wrapper, element);
    wrapper.appendChild(element);
    return wrapper;
  },

  inspect: function(element) {
    element = $(element);
    var result = '<' + element.tagName.toLowerCase();
    $H({'id': 'id', 'className': 'class'}).each(function(pair) {
      var property = pair.first(), attribute = pair.last();
      var value = (element[property] || '').toString();
      if (value) result += ' ' + attribute + '=' + value.inspect(true);
    });
    return result + '>';
  },

  recursivelyCollect: function(element, property) {
    element = $(element);
    var elements = [];
    while (element = element[property])
      if (element.nodeType == 1)
        elements.push(Element.extend(element));
    return elements;
  },

  ancestors: function(element) {
    return $(element).recursivelyCollect('parentNode');
  },

  descendants: function(element) {
    return $(element).select("*");
  },

  firstDescendant: function(element) {
    element = $(element).firstChild;
    while (element && element.nodeType != 1) element = element.nextSibling;
    return $(element);
  },

  immediateDescendants: function(element) {
    if (!(element = $(element).firstChild)) return [];
    while (element && element.nodeType != 1) element = element.nextSibling;
    if (element) return [element].concat($(element).nextSiblings());
    return [];
  },

  previousSiblings: function(element) {
    return $(element).recursivelyCollect('previousSibling');
  },

  nextSiblings: function(element) {
    return $(element).recursivelyCollect('nextSibling');
  },

  siblings: function(element) {
    element = $(element);
    return element.previousSiblings().reverse().concat(element.nextSiblings());
  },

  match: function(element, selector) {
    if (Object.isString(selector))
      selector = new Selector(selector);
    return selector.match($(element));
  },

  up: function(element, expression, index) {
    element = $(element);
    if (arguments.length == 1) return $(element.parentNode);
    var ancestors = element.ancestors();
    return Object.isNumber(expression) ? ancestors[expression] :
      Selector.findElement(ancestors, expression, index);
  },

  down: function(element, expression, index) {
    element = $(element);
    if (arguments.length == 1) return element.firstDescendant();
    return Object.isNumber(expression) ? element.descendants()[expression] :
      Element.select(element, expression)[index || 0];
  },

  previous: function(element, expression, index) {
    element = $(element);
    if (arguments.length == 1) return $(Selector.handlers.previousElementSibling(element));
    var previousSiblings = element.previousSiblings();
    return Object.isNumber(expression) ? previousSiblings[expression] :
      Selector.findElement(previousSiblings, expression, index);
  },

  next: function(element, expression, index) {
    element = $(element);
    if (arguments.length == 1) return $(Selector.handlers.nextElementSibling(element));
    var nextSiblings = element.nextSiblings();
    return Object.isNumber(expression) ? nextSiblings[expression] :
      Selector.findElement(nextSiblings, expression, index);
  },

  select: function() {
    var args = $A(arguments), element = $(args.shift());
    return Selector.findChildElements(element, args);
  },

  adjacent: function() {
    var args = $A(arguments), element = $(args.shift());
    return Selector.findChildElements(element.parentNode, args).without(element);
  },

  identify: function(element) {
    element = $(element);
    var id = element.readAttribute('id'), self = arguments.callee;
    if (id) return id;
    do { id = 'anonymous_element_' + self.counter++ } while ($(id));
    element.writeAttribute('id', id);
    return id;
  },

  readAttribute: function(element, name) {
    element = $(element);
    if (Prototype.Browser.IE) {
      var t = Element._attributeTranslations.read;
      if (t.values[name]) return t.values[name](element, name);
      if (t.names[name]) name = t.names[name];
      if (name.include(':')) {
        return (!element.attributes || !element.attributes[name]) ? null :
         element.attributes[name].value;
      }
    }
    return element.getAttribute(name);
  },

  writeAttribute: function(element, name, value) {
    element = $(element);
    var attributes = { }, t = Element._attributeTranslations.write;

    if (typeof name == 'object') attributes = name;
    else attributes[name] = Object.isUndefined(value) ? true : value;

    for (var attr in attributes) {
      name = t.names[attr] || attr;
      value = attributes[attr];
      if (t.values[attr]) name = t.values[attr](element, value);
      if (value === false || value === null)
        element.removeAttribute(name);
      else if (value === true)
        element.setAttribute(name, name);
      else element.setAttribute(name, value);
    }
    return element;
  },

  getHeight: function(element) {
    return $(element).getDimensions().height;
  },

  getWidth: function(element) {
    return $(element).getDimensions().width;
  },

  classNames: function(element) {
    return new Element.ClassNames(element);
  },

  hasClassName: function(element, className) {
    if (!(element = $(element))) return;
    var elementClassName = element.className;
    return (elementClassName.length > 0 && (elementClassName == className ||
      new RegExp("(^|\\s)" + className + "(\\s|$)").test(elementClassName)));
  },

  addClassName: function(element, className) {
    if (!(element = $(element))) return;
    if (!element.hasClassName(className))
      element.className += (element.className ? ' ' : '') + className;
    return element;
  },

  removeClassName: function(element, className) {
    if (!(element = $(element))) return;
    element.className = element.className.replace(
      new RegExp("(^|\\s+)" + className + "(\\s+|$)"), ' ').strip();
    return element;
  },

  toggleClassName: function(element, className) {
    if (!(element = $(element))) return;
    return element[element.hasClassName(className) ?
      'removeClassName' : 'addClassName'](className);
  },

  // removes whitespace-only text node children
  cleanWhitespace: function(element) {
    element = $(element);
    var node = element.firstChild;
    while (node) {
      var nextNode = node.nextSibling;
      if (node.nodeType == 3 && !/\S/.test(node.nodeValue))
        element.removeChild(node);
      node = nextNode;
    }
    return element;
  },

  empty: function(element) {
    return $(element).innerHTML.blank();
  },

  descendantOf: function(element, ancestor) {
    element = $(element), ancestor = $(ancestor);

    if (element.compareDocumentPosition)
      return (element.compareDocumentPosition(ancestor) & 8) === 8;

    if (ancestor.contains)
      return ancestor.contains(element) && ancestor !== element;

    while (element = element.parentNode)
      if (element == ancestor) return true;

    return false;
  },

  scrollTo: function(element) {
    element = $(element);
    var pos = element.cumulativeOffset();
    window.scrollTo(pos[0], pos[1]);
    return element;
  },

  getStyle: function(element, style) {
    element = $(element);
    style = style == 'float' ? 'cssFloat' : style.camelize();
    var value = element.style[style];
    if (!value || value == 'auto') {
      var css = document.defaultView.getComputedStyle(element, null);
      value = css ? css[style] : null;
    }
    if (style == 'opacity') return value ? parseFloat(value) : 1.0;
    return value == 'auto' ? null : value;
  },

  getOpacity: function(element) {
    return $(element).getStyle('opacity');
  },

  setStyle: function(element, styles) {
    element = $(element);
    var elementStyle = element.style, match;
    if (Object.isString(styles)) {
      element.style.cssText += ';' + styles;
      return styles.include('opacity') ?
        element.setOpacity(styles.match(/opacity:\s*(\d?\.?\d*)/)[1]) : element;
    }
    for (var property in styles)
      if (property == 'opacity') element.setOpacity(styles[property]);
      else
        elementStyle[(property == 'float' || property == 'cssFloat') ?
          (Object.isUndefined(elementStyle.styleFloat) ? 'cssFloat' : 'styleFloat') :
            property] = styles[property];

    return element;
  },

  setOpacity: function(element, value) {
    element = $(element);
    element.style.opacity = (value == 1 || value === '') ? '' :
      (value < 0.00001) ? 0 : value;
    return element;
  },

  getDimensions: function(element) {
    element = $(element);
    var display = element.getStyle('display');
    if (display != 'none' && display != null) // Safari bug
      return {width: element.offsetWidth, height: element.offsetHeight};

    // All *Width and *Height properties give 0 on elements with display none,
    // so enable the element temporarily
    var els = element.style;
    var originalVisibility = els.visibility;
    var originalPosition = els.position;
    var originalDisplay = els.display;
    els.visibility = 'hidden';
    els.position = 'absolute';
    els.display = 'block';
    var originalWidth = element.clientWidth;
    var originalHeight = element.clientHeight;
    els.display = originalDisplay;
    els.position = originalPosition;
    els.visibility = originalVisibility;
    return {width: originalWidth, height: originalHeight};
  },

  makePositioned: function(element) {
    element = $(element);
    var pos = Element.getStyle(element, 'position');
    if (pos == 'static' || !pos) {
      element._madePositioned = true;
      element.style.position = 'relative';
      // Opera returns the offset relative to the positioning context, when an
      // element is position relative but top and left have not been defined
      if (Prototype.Browser.Opera) {
        element.style.top = 0;
        element.style.left = 0;
      }
    }
    return element;
  },

  undoPositioned: function(element) {
    element = $(element);
    if (element._madePositioned) {
      element._madePositioned = undefined;
      element.style.position =
        element.style.top =
        element.style.left =
        element.style.bottom =
        element.style.right = '';
    }
    return element;
  },

  makeClipping: function(element) {
    element = $(element);
    if (element._overflow) return element;
    element._overflow = Element.getStyle(element, 'overflow') || 'auto';
    if (element._overflow !== 'hidden')
      element.style.overflow = 'hidden';
    return element;
  },

  undoClipping: function(element) {
    element = $(element);
    if (!element._overflow) return element;
    element.style.overflow = element._overflow == 'auto' ? '' : element._overflow;
    element._overflow = null;
    return element;
  },

  cumulativeOffset: function(element) {
    var valueT = 0, valueL = 0;
    do {
      valueT += element.offsetTop  || 0;
      valueL += element.offsetLeft || 0;
      element = element.offsetParent;
    } while (element);
    return Element._returnOffset(valueL, valueT);
  },

  positionedOffset: function(element) {
    var valueT = 0, valueL = 0;
    do {
      valueT += element.offsetTop  || 0;
      valueL += element.offsetLeft || 0;
      element = element.offsetParent;
      if (element) {
        if (element.tagName.toUpperCase() == 'BODY') break;
        var p = Element.getStyle(element, 'position');
        if (p !== 'static') break;
      }
    } while (element);
    return Element._returnOffset(valueL, valueT);
  },

  absolutize: function(element) {
    element = $(element);
    if (element.getStyle('position') == 'absolute') return element;
    // Position.prepare(); // To be done manually by Scripty when it needs it.

    var offsets = element.positionedOffset();
    var top     = offsets[1];
    var left    = offsets[0];
    var width   = element.clientWidth;
    var height  = element.clientHeight;

    element._originalLeft   = left - parseFloat(element.style.left  || 0);
    element._originalTop    = top  - parseFloat(element.style.top || 0);
    element._originalWidth  = element.style.width;
    element._originalHeight = element.style.height;

    element.style.position = 'absolute';
    element.style.top    = top + 'px';
    element.style.left   = left + 'px';
    element.style.width  = width + 'px';
    element.style.height = height + 'px';
    return element;
  },

  relativize: function(element) {
    element = $(element);
    if (element.getStyle('position') == 'relative') return element;
    // Position.prepare(); // To be done manually by Scripty when it needs it.

    element.style.position = 'relative';
    var top  = parseFloat(element.style.top  || 0) - (element._originalTop || 0);
    var left = parseFloat(element.style.left || 0) - (element._originalLeft || 0);

    element.style.top    = top + 'px';
    element.style.left   = left + 'px';
    element.style.height = element._originalHeight;
    element.style.width  = element._originalWidth;
    return element;
  },

  cumulativeScrollOffset: function(element) {
    var valueT = 0, valueL = 0;
    do {
      valueT += element.scrollTop  || 0;
      valueL += element.scrollLeft || 0;
      element = element.parentNode;
    } while (element);
    return Element._returnOffset(valueL, valueT);
  },

  getOffsetParent: function(element) {
    if (element.offsetParent) return $(element.offsetParent);
    if (element == document.body) return $(element);

    while ((element = element.parentNode) && element != document.body)
      if (Element.getStyle(element, 'position') != 'static')
        return $(element);

    return $(document.body);
  },

  viewportOffset: function(forElement) {
    var valueT = 0, valueL = 0;

    var element = forElement;
    do {
      valueT += element.offsetTop  || 0;
      valueL += element.offsetLeft || 0;

      // Safari fix
      if (element.offsetParent == document.body &&
        Element.getStyle(element, 'position') == 'absolute') break;

    } while (element = element.offsetParent);

    element = forElement;
    do {
      if (!Prototype.Browser.Opera || (element.tagName && (element.tagName.toUpperCase() == 'BODY'))) {
        valueT -= element.scrollTop  || 0;
        valueL -= element.scrollLeft || 0;
      }
    } while (element = element.parentNode);

    return Element._returnOffset(valueL, valueT);
  },

  clonePosition: function(element, source) {
    var options = Object.extend({
      setLeft:    true,
      setTop:     true,
      setWidth:   true,
      setHeight:  true,
      offsetTop:  0,
      offsetLeft: 0
    }, arguments[2] || { });

    // find page position of source
    source = $(source);
    var p = source.viewportOffset();

    // find coordinate system to use
    element = $(element);
    var delta = [0, 0];
    var parent = null;
    // delta [0,0] will do fine with position: fixed elements,
    // position:absolute needs offsetParent deltas
    if (Element.getStyle(element, 'position') == 'absolute') {
      parent = element.getOffsetParent();
      delta = parent.viewportOffset();
    }

    // correct by body offsets (fixes Safari)
    if (parent == document.body) {
      delta[0] -= document.body.offsetLeft;
      delta[1] -= document.body.offsetTop;
    }

    // set position
    if (options.setLeft)   element.style.left  = (p[0] - delta[0] + options.offsetLeft) + 'px';
    if (options.setTop)    element.style.top   = (p[1] - delta[1] + options.offsetTop) + 'px';
    if (options.setWidth)  element.style.width = source.offsetWidth + 'px';
    if (options.setHeight) element.style.height = source.offsetHeight + 'px';
    return element;
  }
};

Element.Methods.identify.counter = 1;

Object.extend(Element.Methods, {
  getElementsBySelector: Element.Methods.select,
  childElements: Element.Methods.immediateDescendants
});

Element._attributeTranslations = {
  write: {
    names: {
      className: 'class',
      htmlFor:   'for'
    },
    values: { }
  }
};

if (Prototype.Browser.Opera) {
  Element.Methods.getStyle = Element.Methods.getStyle.wrap(
    function(proceed, element, style) {
      switch (style) {
        case 'left': case 'top': case 'right': case 'bottom':
          if (proceed(element, 'position') === 'static') return null;
        case 'height': case 'width':
          // returns '0px' for hidden elements; we want it to return null
          if (!Element.visible(element)) return null;

          // returns the border-box dimensions rather than the content-box
          // dimensions, so we subtract padding and borders from the value
          var dim = parseInt(proceed(element, style), 10);

          if (dim !== element['offset' + style.capitalize()])
            return dim + 'px';

          var properties;
          if (style === 'height') {
            properties = ['border-top-width', 'padding-top',
             'padding-bottom', 'border-bottom-width'];
          }
          else {
            properties = ['border-left-width', 'padding-left',
             'padding-right', 'border-right-width'];
          }
          return properties.inject(dim, function(memo, property) {
            var val = proceed(element, property);
            return val === null ? memo : memo - parseInt(val, 10);
          }) + 'px';
        default: return proceed(element, style);
      }
    }
  );

  Element.Methods.readAttribute = Element.Methods.readAttribute.wrap(
    function(proceed, element, attribute) {
      if (attribute === 'title') return element.title;
      return proceed(element, attribute);
    }
  );
}

else if (Prototype.Browser.IE) {
  // IE doesn't report offsets correctly for static elements, so we change them
  // to "relative" to get the values, then change them back.
  Element.Methods.getOffsetParent = Element.Methods.getOffsetParent.wrap(
    function(proceed, element) {
      element = $(element);
      // IE throws an error if element is not in document
      try { element.offsetParent }
      catch(e) { return $(document.body) }
      var position = element.getStyle('position');
      if (position !== 'static') return proceed(element);
      element.setStyle({ position: 'relative' });
      var value = proceed(element);
      element.setStyle({ position: position });
      return value;
    }
  );

  $w('positionedOffset viewportOffset').each(function(method) {
    Element.Methods[method] = Element.Methods[method].wrap(
      function(proceed, element) {
        element = $(element);
        try { element.offsetParent }
        catch(e) { return Element._returnOffset(0,0) }
        var position = element.getStyle('position');
        if (position !== 'static') return proceed(element);
        // Trigger hasLayout on the offset parent so that IE6 reports
        // accurate offsetTop and offsetLeft values for position: fixed.
        var offsetParent = element.getOffsetParent();
        if (offsetParent && offsetParent.getStyle('position') === 'fixed')
          offsetParent.setStyle({ zoom: 1 });
        element.setStyle({ position: 'relative' });
        var value = proceed(element);
        element.setStyle({ position: position });
        return value;
      }
    );
  });

  Element.Methods.cumulativeOffset = Element.Methods.cumulativeOffset.wrap(
    function(proceed, element) {
      try { element.offsetParent }
      catch(e) { return Element._returnOffset(0,0) }
      return proceed(element);
    }
  );

  Element.Methods.getStyle = function(element, style) {
    element = $(element);
    style = (style == 'float' || style == 'cssFloat') ? 'styleFloat' : style.camelize();
    var value = element.style[style];
    if (!value && element.currentStyle) value = element.currentStyle[style];

    if (style == 'opacity') {
      if (value = (element.getStyle('filter') || '').match(/alpha\(opacity=(.*)\)/))
        if (value[1]) return parseFloat(value[1]) / 100;
      return 1.0;
    }

    if (value == 'auto') {
      if ((style == 'width' || style == 'height') && (element.getStyle('display') != 'none'))
        return element['offset' + style.capitalize()] + 'px';
      return null;
    }
    return value;
  };

  Element.Methods.setOpacity = function(element, value) {
    function stripAlpha(filter){
      return filter.replace(/alpha\([^\)]*\)/gi,'');
    }
    element = $(element);
    var currentStyle = element.currentStyle;
    if ((currentStyle && !currentStyle.hasLayout) ||
      (!currentStyle && element.style.zoom == 'normal'))
        element.style.zoom = 1;

    var filter = element.getStyle('filter'), style = element.style;
    if (value == 1 || value === '') {
      (filter = stripAlpha(filter)) ?
        style.filter = filter : style.removeAttribute('filter');
      return element;
    } else if (value < 0.00001) value = 0;
    style.filter = stripAlpha(filter) +
      'alpha(opacity=' + (value * 100) + ')';
    return element;
  };

  Element._attributeTranslations = {
    read: {
      names: {
        'class': 'className',
        'for':   'htmlFor'
      },
      values: {
        _getAttr: function(element, attribute) {
          return element.getAttribute(attribute, 2);
        },
        _getAttrNode: function(element, attribute) {
          var node = element.getAttributeNode(attribute);
          return node ? node.value : "";
        },
        _getEv: function(element, attribute) {
          attribute = element.getAttribute(attribute);
          return attribute ? attribute.toString().slice(23, -2) : null;
        },
        _flag: function(element, attribute) {
          return $(element).hasAttribute(attribute) ? attribute : null;
        },
        style: function(element) {
          return element.style.cssText.toLowerCase();
        },
        title: function(element) {
          return element.title;
        }
      }
    }
  };

  Element._attributeTranslations.write = {
    names: Object.extend({
      cellpadding: 'cellPadding',
      cellspacing: 'cellSpacing'
    }, Element._attributeTranslations.read.names),
    values: {
      checked: function(element, value) {
        element.checked = !!value;
      },

      style: function(element, value) {
        element.style.cssText = value ? value : '';
      }
    }
  };

  Element._attributeTranslations.has = {};

  $w('colSpan rowSpan vAlign dateTime accessKey tabIndex ' +
      'encType maxLength readOnly longDesc frameBorder').each(function(attr) {
    Element._attributeTranslations.write.names[attr.toLowerCase()] = attr;
    Element._attributeTranslations.has[attr.toLowerCase()] = attr;
  });

  (function(v) {
    Object.extend(v, {
      href:        v._getAttr,
      src:         v._getAttr,
      type:        v._getAttr,
      action:      v._getAttrNode,
      disabled:    v._flag,
      checked:     v._flag,
      readonly:    v._flag,
      multiple:    v._flag,
      onload:      v._getEv,
      onunload:    v._getEv,
      onclick:     v._getEv,
      ondblclick:  v._getEv,
      onmousedown: v._getEv,
      onmouseup:   v._getEv,
      onmouseover: v._getEv,
      onmousemove: v._getEv,
      onmouseout:  v._getEv,
      onfocus:     v._getEv,
      onblur:      v._getEv,
      onkeypress:  v._getEv,
      onkeydown:   v._getEv,
      onkeyup:     v._getEv,
      onsubmit:    v._getEv,
      onreset:     v._getEv,
      onselect:    v._getEv,
      onchange:    v._getEv
    });
  })(Element._attributeTranslations.read.values);
}

else if (Prototype.Browser.Gecko && /rv:1\.8\.0/.test(navigator.userAgent)) {
  Element.Methods.setOpacity = function(element, value) {
    element = $(element);
    element.style.opacity = (value == 1) ? 0.999999 :
      (value === '') ? '' : (value < 0.00001) ? 0 : value;
    return element;
  };
}

else if (Prototype.Browser.WebKit) {
  Element.Methods.setOpacity = function(element, value) {
    element = $(element);
    element.style.opacity = (value == 1 || value === '') ? '' :
      (value < 0.00001) ? 0 : value;

    if (value == 1)
      if(element.tagName.toUpperCase() == 'IMG' && element.width) {
        element.width++; element.width--;
      } else try {
        var n = document.createTextNode(' ');
        element.appendChild(n);
        element.removeChild(n);
      } catch (e) { }

    return element;
  };

  // Safari returns margins on body which is incorrect if the child is absolutely
  // positioned.  For performance reasons, redefine Element#cumulativeOffset for
  // KHTML/WebKit only.
  Element.Methods.cumulativeOffset = function(element) {
    var valueT = 0, valueL = 0;
    do {
      valueT += element.offsetTop  || 0;
      valueL += element.offsetLeft || 0;
      if (element.offsetParent == document.body)
        if (Element.getStyle(element, 'position') == 'absolute') break;

      element = element.offsetParent;
    } while (element);

    return Element._returnOffset(valueL, valueT);
  };
}

if (Prototype.Browser.IE || Prototype.Browser.Opera) {
  // IE and Opera are missing .innerHTML support for TABLE-related and SELECT elements
  Element.Methods.update = function(element, content) {
    element = $(element);

    if (content && content.toElement) content = content.toElement();
    if (Object.isElement(content)) return element.update().insert(content);

    content = Object.toHTML(content);
    var tagName = element.tagName.toUpperCase();

    if (tagName in Element._insertionTranslations.tags) {
      $A(element.childNodes).each(function(node) { element.removeChild(node) });
      Element._getContentFromAnonymousElement(tagName, content.stripScripts())
        .each(function(node) { element.appendChild(node) });
    }
    else element.innerHTML = content.stripScripts();

    content.evalScripts.bind(content).defer();
    return element;
  };
}

if ('outerHTML' in document.createElement('div')) {
  Element.Methods.replace = function(element, content) {
    element = $(element);

    if (content && content.toElement) content = content.toElement();
    if (Object.isElement(content)) {
      element.parentNode.replaceChild(content, element);
      return element;
    }

    content = Object.toHTML(content);
    var parent = element.parentNode, tagName = parent.tagName.toUpperCase();

    if (Element._insertionTranslations.tags[tagName]) {
      var nextSibling = element.next();
      var fragments = Element._getContentFromAnonymousElement(tagName, content.stripScripts());
      parent.removeChild(element);
      if (nextSibling)
        fragments.each(function(node) { parent.insertBefore(node, nextSibling) });
      else
        fragments.each(function(node) { parent.appendChild(node) });
    }
    else element.outerHTML = content.stripScripts();

    content.evalScripts.bind(content).defer();
    return element;
  };
}

Element._returnOffset = function(l, t) {
  var result = [l, t];
  result.left = l;
  result.top = t;
  return result;
};

Element._getContentFromAnonymousElement = function(tagName, html) {
  var div = new Element('div'), t = Element._insertionTranslations.tags[tagName];
  if (t) {
    div.innerHTML = t[0] + html + t[1];
    t[2].times(function() { div = div.firstChild });
  } else div.innerHTML = html;
  return $A(div.childNodes);
};

Element._insertionTranslations = {
  before: function(element, node) {
    element.parentNode.insertBefore(node, element);
  },
  top: function(element, node) {
    element.insertBefore(node, element.firstChild);
  },
  bottom: function(element, node) {
    element.appendChild(node);
  },
  after: function(element, node) {
    element.parentNode.insertBefore(node, element.nextSibling);
  },
  tags: {
    TABLE:  ['<table>',                '</table>',                   1],
    TBODY:  ['<table><tbody>',         '</tbody></table>',           2],
    TR:     ['<table><tbody><tr>',     '</tr></tbody></table>',      3],
    TD:     ['<table><tbody><tr><td>', '</td></tr></tbody></table>', 4],
    SELECT: ['<select>',               '</select>',                  1]
  }
};

(function() {
  Object.extend(this.tags, {
    THEAD: this.tags.TBODY,
    TFOOT: this.tags.TBODY,
    TH:    this.tags.TD
  });
}).call(Element._insertionTranslations);

Element.Methods.Simulated = {
  hasAttribute: function(element, attribute) {
    attribute = Element._attributeTranslations.has[attribute] || attribute;
    var node = $(element).getAttributeNode(attribute);
    return !!(node && node.specified);
  }
};

Element.Methods.ByTag = { };

Object.extend(Element, Element.Methods);

if (!Prototype.BrowserFeatures.ElementExtensions &&
    document.createElement('div')['__proto__']) {
  window.HTMLElement = { };
  window.HTMLElement.prototype = document.createElement('div')['__proto__'];
  Prototype.BrowserFeatures.ElementExtensions = true;
}

Element.extend = (function() {
  if (Prototype.BrowserFeatures.SpecificElementExtensions)
    return Prototype.K;

  var Methods = { }, ByTag = Element.Methods.ByTag;

  var extend = Object.extend(function(element) {
    if (!element || element._extendedByPrototype ||
        element.nodeType != 1 || element == window) return element;

    var methods = Object.clone(Methods),
      tagName = element.tagName.toUpperCase(), property, value;

    // extend methods for specific tags
    if (ByTag[tagName]) Object.extend(methods, ByTag[tagName]);

    for (property in methods) {
      value = methods[property];
      if (Object.isFunction(value) && !(property in element))
        element[property] = value.methodize();
    }

    element._extendedByPrototype = Prototype.emptyFunction;
    return element;

  }, {
    refresh: function() {
      // extend methods for all tags (Safari doesn't need this)
      if (!Prototype.BrowserFeatures.ElementExtensions) {
        Object.extend(Methods, Element.Methods);
        Object.extend(Methods, Element.Methods.Simulated);
      }
    }
  });

  extend.refresh();
  return extend;
})();

Element.hasAttribute = function(element, attribute) {
  if (element.hasAttribute) return element.hasAttribute(attribute);
  return Element.Methods.Simulated.hasAttribute(element, attribute);
};

Element.addMethods = function(methods) {
  var F = Prototype.BrowserFeatures, T = Element.Methods.ByTag;

  if (!methods) {
    Object.extend(Form, Form.Methods);
    Object.extend(Form.Element, Form.Element.Methods);
    Object.extend(Element.Methods.ByTag, {
      "FORM":     Object.clone(Form.Methods),
      "INPUT":    Object.clone(Form.Element.Methods),
      "SELECT":   Object.clone(Form.Element.Methods),
      "TEXTAREA": Object.clone(Form.Element.Methods)
    });
  }

  if (arguments.length == 2) {
    var tagName = methods;
    methods = arguments[1];
  }

  if (!tagName) Object.extend(Element.Methods, methods || { });
  else {
    if (Object.isArray(tagName)) tagName.each(extend);
    else extend(tagName);
  }

  function extend(tagName) {
    tagName = tagName.toUpperCase();
    if (!Element.Methods.ByTag[tagName])
      Element.Methods.ByTag[tagName] = { };
    Object.extend(Element.Methods.ByTag[tagName], methods);
  }

  function copy(methods, destination, onlyIfAbsent) {
    onlyIfAbsent = onlyIfAbsent || false;
    for (var property in methods) {
      var value = methods[property];
      if (!Object.isFunction(value)) continue;
      if (!onlyIfAbsent || !(property in destination))
        destination[property] = value.methodize();
    }
  }

  function findDOMClass(tagName) {
    var klass;
    var trans = {
      "OPTGROUP": "OptGroup", "TEXTAREA": "TextArea", "P": "Paragraph",
      "FIELDSET": "FieldSet", "UL": "UList", "OL": "OList", "DL": "DList",
      "DIR": "Directory", "H1": "Heading", "H2": "Heading", "H3": "Heading",
      "H4": "Heading", "H5": "Heading", "H6": "Heading", "Q": "Quote",
      "INS": "Mod", "DEL": "Mod", "A": "Anchor", "IMG": "Image", "CAPTION":
      "TableCaption", "COL": "TableCol", "COLGROUP": "TableCol", "THEAD":
      "TableSection", "TFOOT": "TableSection", "TBODY": "TableSection", "TR":
      "TableRow", "TH": "TableCell", "TD": "TableCell", "FRAMESET":
      "FrameSet", "IFRAME": "IFrame"
    };
    if (trans[tagName]) klass = 'HTML' + trans[tagName] + 'Element';
    if (window[klass]) return window[klass];
    klass = 'HTML' + tagName + 'Element';
    if (window[klass]) return window[klass];
    klass = 'HTML' + tagName.capitalize() + 'Element';
    if (window[klass]) return window[klass];

    window[klass] = { };
    window[klass].prototype = document.createElement(tagName)['__proto__'];
    return window[klass];
  }

  if (F.ElementExtensions) {
    copy(Element.Methods, HTMLElement.prototype);
    copy(Element.Methods.Simulated, HTMLElement.prototype, true);
  }

  if (F.SpecificElementExtensions) {
    for (var tag in Element.Methods.ByTag) {
      var klass = findDOMClass(tag);
      if (Object.isUndefined(klass)) continue;
      copy(T[tag], klass.prototype);
    }
  }

  Object.extend(Element, Element.Methods);
  delete Element.ByTag;

  if (Element.extend.refresh) Element.extend.refresh();
  Element.cache = { };
};

document.viewport = {
  getDimensions: function() {
    var dimensions = { }, B = Prototype.Browser;
    $w('width height').each(function(d) {
      var D = d.capitalize();
      if (B.WebKit && !document.evaluate) {
        // Safari <3.0 needs self.innerWidth/Height
        dimensions[d] = self['inner' + D];
      } else if (B.Opera && parseFloat(window.opera.version()) < 9.5) {
        // Opera <9.5 needs document.body.clientWidth/Height
        dimensions[d] = document.body['client' + D]
      } else {
        dimensions[d] = document.documentElement['client' + D];
      }
    });
    return dimensions;
  },

  getWidth: function() {
    return this.getDimensions().width;
  },

  getHeight: function() {
    return this.getDimensions().height;
  },

  getScrollOffsets: function() {
    return Element._returnOffset(
      window.pageXOffset || document.documentElement.scrollLeft || document.body.scrollLeft,
      window.pageYOffset || document.documentElement.scrollTop || document.body.scrollTop);
  }
};
/* Portions of the Selector class are derived from Jack Slocum's DomQuery,
 * part of YUI-Ext version 0.40, distributed under the terms of an MIT-style
 * license.  Please see http://www.yui-ext.com/ for more information. */

var Selector = Class.create({
  initialize: function(expression) {
    this.expression = expression.strip();

    if (this.shouldUseSelectorsAPI()) {
      this.mode = 'selectorsAPI';
    } else if (this.shouldUseXPath()) {
      this.mode = 'xpath';
      this.compileXPathMatcher();
    } else {
      this.mode = "normal";
      this.compileMatcher();
    }

  },

  shouldUseXPath: function() {
    if (!Prototype.BrowserFeatures.XPath) return false;

    var e = this.expression;

    // Safari 3 chokes on :*-of-type and :empty
    if (Prototype.Browser.WebKit &&
     (e.include("-of-type") || e.include(":empty")))
      return false;

    // XPath can't do namespaced attributes, nor can it read
    // the "checked" property from DOM nodes
    if ((/(\[[\w-]*?:|:checked)/).test(e))
      return false;

    return true;
  },

  shouldUseSelectorsAPI: function() {
    if (!Prototype.BrowserFeatures.SelectorsAPI) return false;

    if (!Selector._div) Selector._div = new Element('div');

    // Make sure the browser treats the selector as valid. Test on an
    // isolated element to minimize cost of this check.
    try {
      Selector._div.querySelector(this.expression);
    } catch(e) {
      return false;
    }

    return true;
  },

  compileMatcher: function() {
    var e = this.expression, ps = Selector.patterns, h = Selector.handlers,
        c = Selector.criteria, le, p, m;

    if (Selector._cache[e]) {
      this.matcher = Selector._cache[e];
      return;
    }

    this.matcher = ["this.matcher = function(root) {",
                    "var r = root, h = Selector.handlers, c = false, n;"];

    while (e && le != e && (/\S/).test(e)) {
      le = e;
      for (var i in ps) {
        p = ps[i];
        if (m = e.match(p)) {
          this.matcher.push(Object.isFunction(c[i]) ? c[i](m) :
            new Template(c[i]).evaluate(m));
          e = e.replace(m[0], '');
          break;
        }
      }
    }

    this.matcher.push("return h.unique(n);\n}");
    eval(this.matcher.join('\n'));
    Selector._cache[this.expression] = this.matcher;
  },

  compileXPathMatcher: function() {
    var e = this.expression, ps = Selector.patterns,
        x = Selector.xpath, le, m;

    if (Selector._cache[e]) {
      this.xpath = Selector._cache[e]; return;
    }

    this.matcher = ['.//*'];
    while (e && le != e && (/\S/).test(e)) {
      le = e;
      for (var i in ps) {
        if (m = e.match(ps[i])) {
          this.matcher.push(Object.isFunction(x[i]) ? x[i](m) :
            new Template(x[i]).evaluate(m));
          e = e.replace(m[0], '');
          break;
        }
      }
    }

    this.xpath = this.matcher.join('');
    Selector._cache[this.expression] = this.xpath;
  },

  findElements: function(root) {
    root = root || document;
    var e = this.expression, results;

    switch (this.mode) {
      case 'selectorsAPI':
        // querySelectorAll queries document-wide, then filters to descendants
        // of the context element. That's not what we want.
        // Add an explicit context to the selector if necessary.
        if (root !== document) {
          var oldId = root.id, id = $(root).identify();
          e = "#" + id + " " + e;
        }

        results = $A(root.querySelectorAll(e)).map(Element.extend);
        root.id = oldId;

        return results;
      case 'xpath':
        return document._getElementsByXPath(this.xpath, root);
      default:
       return this.matcher(root);
    }
  },

  match: function(element) {
    this.tokens = [];

    var e = this.expression, ps = Selector.patterns, as = Selector.assertions;
    var le, p, m;

    while (e && le !== e && (/\S/).test(e)) {
      le = e;
      for (var i in ps) {
        p = ps[i];
        if (m = e.match(p)) {
          // use the Selector.assertions methods unless the selector
          // is too complex.
          if (as[i]) {
            this.tokens.push([i, Object.clone(m)]);
            e = e.replace(m[0], '');
          } else {
            // reluctantly do a document-wide search
            // and look for a match in the array
            return this.findElements(document).include(element);
          }
        }
      }
    }

    var match = true, name, matches;
    for (var i = 0, token; token = this.tokens[i]; i++) {
      name = token[0], matches = token[1];
      if (!Selector.assertions[name](element, matches)) {
        match = false; break;
      }
    }

    return match;
  },

  toString: function() {
    return this.expression;
  },

  inspect: function() {
    return "#<Selector:" + this.expression.inspect() + ">";
  }
});

Object.extend(Selector, {
  _cache: { },

  xpath: {
    descendant:   "//*",
    child:        "/*",
    adjacent:     "/following-sibling::*[1]",
    laterSibling: '/following-sibling::*',
    tagName:      function(m) {
      if (m[1] == '*') return '';
      return "[local-name()='" + m[1].toLowerCase() +
             "' or local-name()='" + m[1].toUpperCase() + "']";
    },
    className:    "[contains(concat(' ', @class, ' '), ' #{1} ')]",
    id:           "[@id='#{1}']",
    attrPresence: function(m) {
      m[1] = m[1].toLowerCase();
      return new Template("[@#{1}]").evaluate(m);
    },
    attr: function(m) {
      m[1] = m[1].toLowerCase();
      m[3] = m[5] || m[6];
      return new Template(Selector.xpath.operators[m[2]]).evaluate(m);
    },
    pseudo: function(m) {
      var h = Selector.xpath.pseudos[m[1]];
      if (!h) return '';
      if (Object.isFunction(h)) return h(m);
      return new Template(Selector.xpath.pseudos[m[1]]).evaluate(m);
    },
    operators: {
      '=':  "[@#{1}='#{3}']",
      '!=': "[@#{1}!='#{3}']",
      '^=': "[starts-with(@#{1}, '#{3}')]",
      '$=': "[substring(@#{1}, (string-length(@#{1}) - string-length('#{3}') + 1))='#{3}']",
      '*=': "[contains(@#{1}, '#{3}')]",
      '~=': "[contains(concat(' ', @#{1}, ' '), ' #{3} ')]",
      '|=': "[contains(concat('-', @#{1}, '-'), '-#{3}-')]"
    },
    pseudos: {
      'first-child': '[not(preceding-sibling::*)]',
      'last-child':  '[not(following-sibling::*)]',
      'only-child':  '[not(preceding-sibling::* or following-sibling::*)]',
      'empty':       "[count(*) = 0 and (count(text()) = 0)]",
      'checked':     "[@checked]",
      'disabled':    "[(@disabled) and (@type!='hidden')]",
      'enabled':     "[not(@disabled) and (@type!='hidden')]",
      'not': function(m) {
        var e = m[6], p = Selector.patterns,
            x = Selector.xpath, le, v;

        var exclusion = [];
        while (e && le != e && (/\S/).test(e)) {
          le = e;
          for (var i in p) {
            if (m = e.match(p[i])) {
              v = Object.isFunction(x[i]) ? x[i](m) : new Template(x[i]).evaluate(m);
              exclusion.push("(" + v.substring(1, v.length - 1) + ")");
              e = e.replace(m[0], '');
              break;
            }
          }
        }
        return "[not(" + exclusion.join(" and ") + ")]";
      },
      'nth-child':      function(m) {
        return Selector.xpath.pseudos.nth("(count(./preceding-sibling::*) + 1) ", m);
      },
      'nth-last-child': function(m) {
        return Selector.xpath.pseudos.nth("(count(./following-sibling::*) + 1) ", m);
      },
      'nth-of-type':    function(m) {
        return Selector.xpath.pseudos.nth("position() ", m);
      },
      'nth-last-of-type': function(m) {
        return Selector.xpath.pseudos.nth("(last() + 1 - position()) ", m);
      },
      'first-of-type':  function(m) {
        m[6] = "1"; return Selector.xpath.pseudos['nth-of-type'](m);
      },
      'last-of-type':   function(m) {
        m[6] = "1"; return Selector.xpath.pseudos['nth-last-of-type'](m);
      },
      'only-of-type':   function(m) {
        var p = Selector.xpath.pseudos; return p['first-of-type'](m) + p['last-of-type'](m);
      },
      nth: function(fragment, m) {
        var mm, formula = m[6], predicate;
        if (formula == 'even') formula = '2n+0';
        if (formula == 'odd')  formula = '2n+1';
        if (mm = formula.match(/^(\d+)$/)) // digit only
          return '[' + fragment + "= " + mm[1] + ']';
        if (mm = formula.match(/^(-?\d*)?n(([+-])(\d+))?/)) { // an+b
          if (mm[1] == "-") mm[1] = -1;
          var a = mm[1] ? Number(mm[1]) : 1;
          var b = mm[2] ? Number(mm[2]) : 0;
          predicate = "[((#{fragment} - #{b}) mod #{a} = 0) and " +
          "((#{fragment} - #{b}) div #{a} >= 0)]";
          return new Template(predicate).evaluate({
            fragment: fragment, a: a, b: b });
        }
      }
    }
  },

  criteria: {
    tagName:      'n = h.tagName(n, r, "#{1}", c);      c = false;',
    className:    'n = h.className(n, r, "#{1}", c);    c = false;',
    id:           'n = h.id(n, r, "#{1}", c);           c = false;',
    attrPresence: 'n = h.attrPresence(n, r, "#{1}", c); c = false;',
    attr: function(m) {
      m[3] = (m[5] || m[6]);
      return new Template('n = h.attr(n, r, "#{1}", "#{3}", "#{2}", c); c = false;').evaluate(m);
    },
    pseudo: function(m) {
      if (m[6]) m[6] = m[6].replace(/"/g, '\\"');
      return new Template('n = h.pseudo(n, "#{1}", "#{6}", r, c); c = false;').evaluate(m);
    },
    descendant:   'c = "descendant";',
    child:        'c = "child";',
    adjacent:     'c = "adjacent";',
    laterSibling: 'c = "laterSibling";'
  },

  patterns: {
    // combinators must be listed first
    // (and descendant needs to be last combinator)
    laterSibling: /^\s*~\s*/,
    child:        /^\s*>\s*/,
    adjacent:     /^\s*\+\s*/,
    descendant:   /^\s/,

    // selectors follow
    tagName:      /^\s*(\*|[\w\-]+)(\b|$)?/,
    id:           /^#([\w\-\*]+)(\b|$)/,
    className:    /^\.([\w\-\*]+)(\b|$)/,
    pseudo:
/^:((first|last|nth|nth-last|only)(-child|-of-type)|empty|checked|(en|dis)abled|not)(\((.*?)\))?(\b|$|(?=\s|[:+~>]))/,
    attrPresence: /^\[((?:[\w]+:)?[\w]+)\]/,
    attr:         /\[((?:[\w-]*:)?[\w-]+)\s*(?:([!^$*~|]?=)\s*((['"])([^\4]*?)\4|([^'"][^\]]*?)))?\]/
  },

  // for Selector.match and Element#match
  assertions: {
    tagName: function(element, matches) {
      return matches[1].toUpperCase() == element.tagName.toUpperCase();
    },

    className: function(element, matches) {
      return Element.hasClassName(element, matches[1]);
    },

    id: function(element, matches) {
      return element.id === matches[1];
    },

    attrPresence: function(element, matches) {
      return Element.hasAttribute(element, matches[1]);
    },

    attr: function(element, matches) {
      var nodeValue = Element.readAttribute(element, matches[1]);
      return nodeValue && Selector.operators[matches[2]](nodeValue, matches[5] || matches[6]);
    }
  },

  handlers: {
    // UTILITY FUNCTIONS
    // joins two collections
    concat: function(a, b) {
      for (var i = 0, node; node = b[i]; i++)
        a.push(node);
      return a;
    },

    // marks an array of nodes for counting
    mark: function(nodes) {
      var _true = Prototype.emptyFunction;
      for (var i = 0, node; node = nodes[i]; i++)
        node._countedByPrototype = _true;
      return nodes;
    },

    unmark: function(nodes) {
      for (var i = 0, node; node = nodes[i]; i++)
        node._countedByPrototype = undefined;
      return nodes;
    },

    // mark each child node with its position (for nth calls)
    // "ofType" flag indicates whether we're indexing for nth-of-type
    // rather than nth-child
    index: function(parentNode, reverse, ofType) {
      parentNode._countedByPrototype = Prototype.emptyFunction;
      if (reverse) {
        for (var nodes = parentNode.childNodes, i = nodes.length - 1, j = 1; i >= 0; i--) {
          var node = nodes[i];
          if (node.nodeType == 1 && (!ofType || node._countedByPrototype)) node.nodeIndex = j++;
        }
      } else {
        for (var i = 0, j = 1, nodes = parentNode.childNodes; node = nodes[i]; i++)
          if (node.nodeType == 1 && (!ofType || node._countedByPrototype)) node.nodeIndex = j++;
      }
    },

    // filters out duplicates and extends all nodes
    unique: function(nodes) {
      if (nodes.length == 0) return nodes;
      var results = [], n;
      for (var i = 0, l = nodes.length; i < l; i++)
        if (!(n = nodes[i])._countedByPrototype) {
          n._countedByPrototype = Prototype.emptyFunction;
          results.push(Element.extend(n));
        }
      return Selector.handlers.unmark(results);
    },

    // COMBINATOR FUNCTIONS
    descendant: function(nodes) {
      var h = Selector.handlers;
      for (var i = 0, results = [], node; node = nodes[i]; i++)
        h.concat(results, node.getElementsByTagName('*'));
      return results;
    },

    child: function(nodes) {
      var h = Selector.handlers;
      for (var i = 0, results = [], node; node = nodes[i]; i++) {
        for (var j = 0, child; child = node.childNodes[j]; j++)
          if (child.nodeType == 1 && child.tagName != '!') results.push(child);
      }
      return results;
    },

    adjacent: function(nodes) {
      for (var i = 0, results = [], node; node = nodes[i]; i++) {
        var next = this.nextElementSibling(node);
        if (next) results.push(next);
      }
      return results;
    },

    laterSibling: function(nodes) {
      var h = Selector.handlers;
      for (var i = 0, results = [], node; node = nodes[i]; i++)
        h.concat(results, Element.nextSiblings(node));
      return results;
    },

    nextElementSibling: function(node) {
      while (node = node.nextSibling)
        if (node.nodeType == 1) return node;
      return null;
    },

    previousElementSibling: function(node) {
      while (node = node.previousSibling)
        if (node.nodeType == 1) return node;
      return null;
    },

    // TOKEN FUNCTIONS
    tagName: function(nodes, root, tagName, combinator) {
      var uTagName = tagName.toUpperCase();
      var results = [], h = Selector.handlers;
      if (nodes) {
        if (combinator) {
          // fastlane for ordinary descendant combinators
          if (combinator == "descendant") {
            for (var i = 0, node; node = nodes[i]; i++)
              h.concat(results, node.getElementsByTagName(tagName));
            return results;
          } else nodes = this[combinator](nodes);
          if (tagName == "*") return nodes;
        }
        for (var i = 0, node; node = nodes[i]; i++)
          if (node.tagName.toUpperCase() === uTagName) results.push(node);
        return results;
      } else return root.getElementsByTagName(tagName);
    },

    id: function(nodes, root, id, combinator) {
      var targetNode = $(id), h = Selector.handlers;
      if (!targetNode) return [];
      if (!nodes && root == document) return [targetNode];
      if (nodes) {
        if (combinator) {
          if (combinator == 'child') {
            for (var i = 0, node; node = nodes[i]; i++)
              if (targetNode.parentNode == node) return [targetNode];
          } else if (combinator == 'descendant') {
            for (var i = 0, node; node = nodes[i]; i++)
              if (Element.descendantOf(targetNode, node)) return [targetNode];
          } else if (combinator == 'adjacent') {
            for (var i = 0, node; node = nodes[i]; i++)
              if (Selector.handlers.previousElementSibling(targetNode) == node)
                return [targetNode];
          } else nodes = h[combinator](nodes);
        }
        for (var i = 0, node; node = nodes[i]; i++)
          if (node == targetNode) return [targetNode];
        return [];
      }
      return (targetNode && Element.descendantOf(targetNode, root)) ? [targetNode] : [];
    },

    className: function(nodes, root, className, combinator) {
      if (nodes && combinator) nodes = this[combinator](nodes);
      return Selector.handlers.byClassName(nodes, root, className);
    },

    byClassName: function(nodes, root, className) {
      if (!nodes) nodes = Selector.handlers.descendant([root]);
      var needle = ' ' + className + ' ';
      for (var i = 0, results = [], node, nodeClassName; node = nodes[i]; i++) {
        nodeClassName = node.className;
        if (nodeClassName.length == 0) continue;
        if (nodeClassName == className || (' ' + nodeClassName + ' ').include(needle))
          results.push(node);
      }
      return results;
    },

    attrPresence: function(nodes, root, attr, combinator) {
      if (!nodes) nodes = root.getElementsByTagName("*");
      if (nodes && combinator) nodes = this[combinator](nodes);
      var results = [];
      for (var i = 0, node; node = nodes[i]; i++)
        if (Element.hasAttribute(node, attr)) results.push(node);
      return results;
    },

    attr: function(nodes, root, attr, value, operator, combinator) {
      if (!nodes) nodes = root.getElementsByTagName("*");
      if (nodes && combinator) nodes = this[combinator](nodes);
      var handler = Selector.operators[operator], results = [];
      for (var i = 0, node; node = nodes[i]; i++) {
        var nodeValue = Element.readAttribute(node, attr);
        if (nodeValue === null) continue;
        if (handler(nodeValue, value)) results.push(node);
      }
      return results;
    },

    pseudo: function(nodes, name, value, root, combinator) {
      if (nodes && combinator) nodes = this[combinator](nodes);
      if (!nodes) nodes = root.getElementsByTagName("*");
      return Selector.pseudos[name](nodes, value, root);
    }
  },

  pseudos: {
    'first-child': function(nodes, value, root) {
      for (var i = 0, results = [], node; node = nodes[i]; i++) {
        if (Selector.handlers.previousElementSibling(node)) continue;
          results.push(node);
      }
      return results;
    },
    'last-child': function(nodes, value, root) {
      for (var i = 0, results = [], node; node = nodes[i]; i++) {
        if (Selector.handlers.nextElementSibling(node)) continue;
          results.push(node);
      }
      return results;
    },
    'only-child': function(nodes, value, root) {
      var h = Selector.handlers;
      for (var i = 0, results = [], node; node = nodes[i]; i++)
        if (!h.previousElementSibling(node) && !h.nextElementSibling(node))
          results.push(node);
      return results;
    },
    'nth-child':        function(nodes, formula, root) {
      return Selector.pseudos.nth(nodes, formula, root);
    },
    'nth-last-child':   function(nodes, formula, root) {
      return Selector.pseudos.nth(nodes, formula, root, true);
    },
    'nth-of-type':      function(nodes, formula, root) {
      return Selector.pseudos.nth(nodes, formula, root, false, true);
    },
    'nth-last-of-type': function(nodes, formula, root) {
      return Selector.pseudos.nth(nodes, formula, root, true, true);
    },
    'first-of-type':    function(nodes, formula, root) {
      return Selector.pseudos.nth(nodes, "1", root, false, true);
    },
    'last-of-type':     function(nodes, formula, root) {
      return Selector.pseudos.nth(nodes, "1", root, true, true);
    },
    'only-of-type':     function(nodes, formula, root) {
      var p = Selector.pseudos;
      return p['last-of-type'](p['first-of-type'](nodes, formula, root), formula, root);
    },

    // handles the an+b logic
    getIndices: function(a, b, total) {
      if (a == 0) return b > 0 ? [b] : [];
      return $R(1, total).inject([], function(memo, i) {
        if (0 == (i - b) % a && (i - b) / a >= 0) memo.push(i);
        return memo;
      });
    },

    // handles nth(-last)-child, nth(-last)-of-type, and (first|last)-of-type
    nth: function(nodes, formula, root, reverse, ofType) {
      if (nodes.length == 0) return [];
      if (formula == 'even') formula = '2n+0';
      if (formula == 'odd')  formula = '2n+1';
      var h = Selector.handlers, results = [], indexed = [], m;
      h.mark(nodes);
      for (var i = 0, node; node = nodes[i]; i++) {
        if (!node.parentNode._countedByPrototype) {
          h.index(node.parentNode, reverse, ofType);
          indexed.push(node.parentNode);
        }
      }
      if (formula.match(/^\d+$/)) { // just a number
        formula = Number(formula);
        for (var i = 0, node; node = nodes[i]; i++)
          if (node.nodeIndex == formula) results.push(node);
      } else if (m = formula.match(/^(-?\d*)?n(([+-])(\d+))?/)) { // an+b
        if (m[1] == "-") m[1] = -1;
        var a = m[1] ? Number(m[1]) : 1;
        var b = m[2] ? Number(m[2]) : 0;
        var indices = Selector.pseudos.getIndices(a, b, nodes.length);
        for (var i = 0, node, l = indices.length; node = nodes[i]; i++) {
          for (var j = 0; j < l; j++)
            if (node.nodeIndex == indices[j]) results.push(node);
        }
      }
      h.unmark(nodes);
      h.unmark(indexed);
      return results;
    },

    'empty': function(nodes, value, root) {
      for (var i = 0, results = [], node; node = nodes[i]; i++) {
        // IE treats comments as element nodes
        if (node.tagName == '!' || node.firstChild) continue;
        results.push(node);
      }
      return results;
    },

    'not': function(nodes, selector, root) {
      var h = Selector.handlers, selectorType, m;
      var exclusions = new Selector(selector).findElements(root);
      h.mark(exclusions);
      for (var i = 0, results = [], node; node = nodes[i]; i++)
        if (!node._countedByPrototype) results.push(node);
      h.unmark(exclusions);
      return results;
    },

    'enabled': function(nodes, value, root) {
      for (var i = 0, results = [], node; node = nodes[i]; i++)
        if (!node.disabled && (!node.type || node.type !== 'hidden'))
          results.push(node);
      return results;
    },

    'disabled': function(nodes, value, root) {
      for (var i = 0, results = [], node; node = nodes[i]; i++)
        if (node.disabled) results.push(node);
      return results;
    },

    'checked': function(nodes, value, root) {
      for (var i = 0, results = [], node; node = nodes[i]; i++)
        if (node.checked) results.push(node);
      return results;
    }
  },

  operators: {
    '=':  function(nv, v) { return nv == v; },
    '!=': function(nv, v) { return nv != v; },
    '^=': function(nv, v) { return nv == v || nv && nv.startsWith(v); },
    '$=': function(nv, v) { return nv == v || nv && nv.endsWith(v); },
    '*=': function(nv, v) { return nv == v || nv && nv.include(v); },
    '$=': function(nv, v) { return nv.endsWith(v); },
    '*=': function(nv, v) { return nv.include(v); },
    '~=': function(nv, v) { return (' ' + nv + ' ').include(' ' + v + ' '); },
    '|=': function(nv, v) { return ('-' + (nv || "").toUpperCase() +
     '-').include('-' + (v || "").toUpperCase() + '-'); }
  },

  split: function(expression) {
    var expressions = [];
    expression.scan(/(([\w#:.~>+()\s-]+|\*|\[.*?\])+)\s*(,|$)/, function(m) {
      expressions.push(m[1].strip());
    });
    return expressions;
  },

  matchElements: function(elements, expression) {
    var matches = $$(expression), h = Selector.handlers;
    h.mark(matches);
    for (var i = 0, results = [], element; element = elements[i]; i++)
      if (element._countedByPrototype) results.push(element);
    h.unmark(matches);
    return results;
  },

  findElement: function(elements, expression, index) {
    if (Object.isNumber(expression)) {
      index = expression; expression = false;
    }
    return Selector.matchElements(elements, expression || '*')[index || 0];
  },

  findChildElements: function(element, expressions) {
    expressions = Selector.split(expressions.join(','));
    var results = [], h = Selector.handlers;
    for (var i = 0, l = expressions.length, selector; i < l; i++) {
      selector = new Selector(expressions[i].strip());
      h.concat(results, selector.findElements(element));
    }
    return (l > 1) ? h.unique(results) : results;
  }
});

if (Prototype.Browser.IE) {
  Object.extend(Selector.handlers, {
    // IE returns comment nodes on getElementsByTagName("*").
    // Filter them out.
    concat: function(a, b) {
      for (var i = 0, node; node = b[i]; i++)
        if (node.tagName !== "!") a.push(node);
      return a;
    },

    // IE improperly serializes _countedByPrototype in (inner|outer)HTML.
    unmark: function(nodes) {
      for (var i = 0, node; node = nodes[i]; i++)
        node.removeAttribute('_countedByPrototype');
      return nodes;
    }
  });
}

function $$() {
  return Selector.findChildElements(document, $A(arguments));
}
var Form = {
  reset: function(form) {
    $(form).reset();
    return form;
  },

  serializeElements: function(elements, options) {
    if (typeof options != 'object') options = { hash: !!options };
    else if (Object.isUndefined(options.hash)) options.hash = true;
    var key, value, submitted = false, submit = options.submit;

    var data = elements.inject({ }, function(result, element) {
      if (!element.disabled && element.name) {
        key = element.name; value = $(element).getValue();
        if (value != null && element.type != 'file' && (element.type != 'submit' || (!submitted &&
            submit !== false && (!submit || key == submit) && (submitted = true)))) {
          if (key in result) {
            // a key is already present; construct an array of values
            if (!Object.isArray(result[key])) result[key] = [result[key]];
            result[key].push(value);
          }
          else result[key] = value;
        }
      }
      return result;
    });

    return options.hash ? data : Object.toQueryString(data);
  }
};

Form.Methods = {
  serialize: function(form, options) {
    return Form.serializeElements(Form.getElements(form), options);
  },

  getElements: function(form) {
    return $A($(form).getElementsByTagName('*')).inject([],
      function(elements, child) {
        if (Form.Element.Serializers[child.tagName.toLowerCase()])
          elements.push(Element.extend(child));
        return elements;
      }
    );
  },

  getInputs: function(form, typeName, name) {
    form = $(form);
    var inputs = form.getElementsByTagName('input');

    if (!typeName && !name) return $A(inputs).map(Element.extend);

    for (var i = 0, matchingInputs = [], length = inputs.length; i < length; i++) {
      var input = inputs[i];
      if ((typeName && input.type != typeName) || (name && input.name != name))
        continue;
      matchingInputs.push(Element.extend(input));
    }

    return matchingInputs;
  },

  disable: function(form) {
    form = $(form);
    Form.getElements(form).invoke('disable');
    return form;
  },

  enable: function(form) {
    form = $(form);
    Form.getElements(form).invoke('enable');
    return form;
  },

  findFirstElement: function(form) {
    var elements = $(form).getElements().findAll(function(element) {
      return 'hidden' != element.type && !element.disabled;
    });
    var firstByIndex = elements.findAll(function(element) {
      return element.hasAttribute('tabIndex') && element.tabIndex >= 0;
    }).sortBy(function(element) { return element.tabIndex }).first();

    return firstByIndex ? firstByIndex : elements.find(function(element) {
      return ['input', 'select', 'textarea'].include(element.tagName.toLowerCase());
    });
  },

  focusFirstElement: function(form) {
    form = $(form);
    form.findFirstElement().activate();
    return form;
  },

  request: function(form, options) {
    form = $(form), options = Object.clone(options || { });

    var params = options.parameters, action = form.readAttribute('action') || '';
    if (action.blank()) action = window.location.href;
    options.parameters = form.serialize(true);

    if (params) {
      if (Object.isString(params)) params = params.toQueryParams();
      Object.extend(options.parameters, params);
    }

    if (form.hasAttribute('method') && !options.method)
      options.method = form.method;

    return new Ajax.Request(action, options);
  }
};

/*--------------------------------------------------------------------------*/

Form.Element = {
  focus: function(element) {
    $(element).focus();
    return element;
  },

  select: function(element) {
    $(element).select();
    return element;
  }
};

Form.Element.Methods = {
  serialize: function(element) {
    element = $(element);
    if (!element.disabled && element.name) {
      var value = element.getValue();
      if (value != undefined) {
        var pair = { };
        pair[element.name] = value;
        return Object.toQueryString(pair);
      }
    }
    return '';
  },

  getValue: function(element) {
    element = $(element);
    var method = element.tagName.toLowerCase();
    return Form.Element.Serializers[method](element);
  },

  setValue: function(element, value) {
    element = $(element);
    var method = element.tagName.toLowerCase();
    Form.Element.Serializers[method](element, value);
    return element;
  },

  clear: function(element) {
    $(element).value = '';
    return element;
  },

  present: function(element) {
    return $(element).value != '';
  },

  activate: function(element) {
    element = $(element);
    try {
      element.focus();
      if (element.select && (element.tagName.toLowerCase() != 'input' ||
          !['button', 'reset', 'submit'].include(element.type)))
        element.select();
    } catch (e) { }
    return element;
  },

  disable: function(element) {
    element = $(element);
    element.disabled = true;
    return element;
  },

  enable: function(element) {
    element = $(element);
    element.disabled = false;
    return element;
  }
};

/*--------------------------------------------------------------------------*/

var Field = Form.Element;
var $F = Form.Element.Methods.getValue;

/*--------------------------------------------------------------------------*/

Form.Element.Serializers = {
  input: function(element, value) {
    switch (element.type.toLowerCase()) {
      case 'checkbox':
      case 'radio':
        return Form.Element.Serializers.inputSelector(element, value);
      default:
        return Form.Element.Serializers.textarea(element, value);
    }
  },

  inputSelector: function(element, value) {
    if (Object.isUndefined(value)) return element.checked ? element.value : null;
    else element.checked = !!value;
  },

  textarea: function(element, value) {
    if (Object.isUndefined(value)) return element.value;
    else element.value = value;
  },

  select: function(element, value) {
    if (Object.isUndefined(value))
      return this[element.type == 'select-one' ?
        'selectOne' : 'selectMany'](element);
    else {
      var opt, currentValue, single = !Object.isArray(value);
      for (var i = 0, length = element.length; i < length; i++) {
        opt = element.options[i];
        currentValue = this.optionValue(opt);
        if (single) {
          if (currentValue == value) {
            opt.selected = true;
            return;
          }
        }
        else opt.selected = value.include(currentValue);
      }
    }
  },

  selectOne: function(element) {
    var index = element.selectedIndex;
    return index >= 0 ? this.optionValue(element.options[index]) : null;
  },

  selectMany: function(element) {
    var values, length = element.length;
    if (!length) return null;

    for (var i = 0, values = []; i < length; i++) {
      var opt = element.options[i];
      if (opt.selected) values.push(this.optionValue(opt));
    }
    return values;
  },

  optionValue: function(opt) {
    // extend element because hasAttribute may not be native
    return Element.extend(opt).hasAttribute('value') ? opt.value : opt.text;
  }
};

/*--------------------------------------------------------------------------*/

Abstract.TimedObserver = Class.create(PeriodicalExecuter, {
  initialize: function($super, element, frequency, callback) {
    $super(callback, frequency);
    this.element   = $(element);
    this.lastValue = this.getValue();
  },

  execute: function() {
    var value = this.getValue();
    if (Object.isString(this.lastValue) && Object.isString(value) ?
        this.lastValue != value : String(this.lastValue) != String(value)) {
      this.callback(this.element, value);
      this.lastValue = value;
    }
  }
});

Form.Element.Observer = Class.create(Abstract.TimedObserver, {
  getValue: function() {
    return Form.Element.getValue(this.element);
  }
});

Form.Observer = Class.create(Abstract.TimedObserver, {
  getValue: function() {
    return Form.serialize(this.element);
  }
});

/*--------------------------------------------------------------------------*/

Abstract.EventObserver = Class.create({
  initialize: function(element, callback) {
    this.element  = $(element);
    this.callback = callback;

    this.lastValue = this.getValue();
    if (this.element.tagName.toLowerCase() == 'form')
      this.registerFormCallbacks();
    else
      this.registerCallback(this.element);
  },

  onElementEvent: function() {
    var value = this.getValue();
    if (this.lastValue != value) {
      this.callback(this.element, value);
      this.lastValue = value;
    }
  },

  registerFormCallbacks: function() {
    Form.getElements(this.element).each(this.registerCallback, this);
  },

  registerCallback: function(element) {
    if (element.type) {
      switch (element.type.toLowerCase()) {
        case 'checkbox':
        case 'radio':
          Event.observe(element, 'click', this.onElementEvent.bind(this));
          break;
        default:
          Event.observe(element, 'change', this.onElementEvent.bind(this));
          break;
      }
    }
  }
});

Form.Element.EventObserver = Class.create(Abstract.EventObserver, {
  getValue: function() {
    return Form.Element.getValue(this.element);
  }
});

Form.EventObserver = Class.create(Abstract.EventObserver, {
  getValue: function() {
    return Form.serialize(this.element);
  }
});
if (!window.Event) var Event = { };

Object.extend(Event, {
  KEY_BACKSPACE: 8,
  KEY_TAB:       9,
  KEY_RETURN:   13,
  KEY_ESC:      27,
  KEY_LEFT:     37,
  KEY_UP:       38,
  KEY_RIGHT:    39,
  KEY_DOWN:     40,
  KEY_DELETE:   46,
  KEY_HOME:     36,
  KEY_END:      35,
  KEY_PAGEUP:   33,
  KEY_PAGEDOWN: 34,
  KEY_INSERT:   45,

  cache: { },

  relatedTarget: function(event) {
    var element;
    switch(event.type) {
      case 'mouseover': element = event.fromElement; break;
      case 'mouseout':  element = event.toElement;   break;
      default: return null;
    }
    return Element.extend(element);
  }
});

Event.Methods = (function() {
  var isButton;

  if (Prototype.Browser.IE) {
    var buttonMap = { 0: 1, 1: 4, 2: 2 };
    isButton = function(event, code) {
      return event.button == buttonMap[code];
    };

  } else if (Prototype.Browser.WebKit) {
    isButton = function(event, code) {
      switch (code) {
        case 0: return event.which == 1 && !event.metaKey;
        case 1: return event.which == 1 && event.metaKey;
        default: return false;
      }
    };

  } else {
    isButton = function(event, code) {
      return event.which ? (event.which === code + 1) : (event.button === code);
    };
  }

  return {
    isLeftClick:   function(event) { return isButton(event, 0) },
    isMiddleClick: function(event) { return isButton(event, 1) },
    isRightClick:  function(event) { return isButton(event, 2) },

    element: function(event) {
      event = Event.extend(event);

      var node          = event.target,
          type          = event.type,
          currentTarget = event.currentTarget;

      if (currentTarget && currentTarget.tagName) {
        // Firefox screws up the "click" event when moving between radio buttons
        // via arrow keys. It also screws up the "load" and "error" events on images,
        // reporting the document as the target instead of the original image.
        if (type === 'load' || type === 'error' ||
          (type === 'click' && currentTarget.tagName.toLowerCase() === 'input'
            && currentTarget.type === 'radio'))
              node = currentTarget;
      }
      if (node.nodeType == Node.TEXT_NODE) node = node.parentNode;
      return Element.extend(node);
    },

    findElement: function(event, expression) {
      var element = Event.element(event);
      if (!expression) return element;
      var elements = [element].concat(element.ancestors());
      return Selector.findElement(elements, expression, 0);
    },

    pointer: function(event) {
      var docElement = document.documentElement,
      body = document.body || { scrollLeft: 0, scrollTop: 0 };
      return {
        x: event.pageX || (event.clientX +
          (docElement.scrollLeft || body.scrollLeft) -
          (docElement.clientLeft || 0)),
        y: event.pageY || (event.clientY +
          (docElement.scrollTop || body.scrollTop) -
          (docElement.clientTop || 0))
      };
    },

    pointerX: function(event) { return Event.pointer(event).x },
    pointerY: function(event) { return Event.pointer(event).y },

    stop: function(event) {
      Event.extend(event);
      event.preventDefault();
      event.stopPropagation();
      event.stopped = true;
    }
  };
})();

Event.extend = (function() {
  var methods = Object.keys(Event.Methods).inject({ }, function(m, name) {
    m[name] = Event.Methods[name].methodize();
    return m;
  });

  if (Prototype.Browser.IE) {
    Object.extend(methods, {
      stopPropagation: function() { this.cancelBubble = true },
      preventDefault:  function() { this.returnValue = false },
      inspect: function() { return "[object Event]" }
    });

    return function(event) {
      if (!event) return false;
      if (event._extendedByPrototype) return event;

      event._extendedByPrototype = Prototype.emptyFunction;
      var pointer = Event.pointer(event);
      Object.extend(event, {
        target: event.srcElement,
        relatedTarget: Event.relatedTarget(event),
        pageX:  pointer.x,
        pageY:  pointer.y
      });
      return Object.extend(event, methods);
    };

  } else {
    Event.prototype = Event.prototype || document.createEvent("HTMLEvents")['__proto__'];
    Object.extend(Event.prototype, methods);
    return Prototype.K;
  }
})();

Object.extend(Event, (function() {
  var cache = Event.cache;

  function getEventID(element) {
    if (element._prototypeEventID) return element._prototypeEventID[0];
    arguments.callee.id = arguments.callee.id || 1;
    return element._prototypeEventID = [++arguments.callee.id];
  }

  function getDOMEventName(eventName) {
    if (eventName && eventName.include(':')) return "dataavailable";
    return eventName;
  }

  function getCacheForID(id) {
    return cache[id] = cache[id] || { };
  }

  function getWrappersForEventName(id, eventName) {
    var c = getCacheForID(id);
    return c[eventName] = c[eventName] || [];
  }

  function createWrapper(element, eventName, handler) {
    var id = getEventID(element);
    var c = getWrappersForEventName(id, eventName);
    if (c.pluck("handler").include(handler)) return false;

    var wrapper = function(event) {
      if (!Event || !Event.extend ||
        (event.eventName && event.eventName != eventName))
          return false;

      Event.extend(event);
      handler.call(element, event);
    };

    wrapper.handler = handler;
    c.push(wrapper);
    return wrapper;
  }

  function findWrapper(id, eventName, handler) {
    var c = getWrappersForEventName(id, eventName);
    return c.find(function(wrapper) { return wrapper.handler == handler });
  }

  function destroyWrapper(id, eventName, handler) {
    var c = getCacheForID(id);
    if (!c[eventName]) return false;
    c[eventName] = c[eventName].without(findWrapper(id, eventName, handler));
  }

  function destroyCache() {
    for (var id in cache)
      for (var eventName in cache[id])
        cache[id][eventName] = null;
  }


  // Internet Explorer needs to remove event handlers on page unload
  // in order to avoid memory leaks.
  if (window.attachEvent) {
    window.attachEvent("onunload", destroyCache);
  }

  // Safari has a dummy event handler on page unload so that it won't
  // use its bfcache. Safari <= 3.1 has an issue with restoring the "document"
  // object when page is returned to via the back button using its bfcache.
  if (Prototype.Browser.WebKit) {
    window.addEventListener('unload', Prototype.emptyFunction, false);
  }

  return {
    observe: function(element, eventName, handler) {
      element = $(element);
      var name = getDOMEventName(eventName);

      var wrapper = createWrapper(element, eventName, handler);
      if (!wrapper) return element;

      if (element.addEventListener) {
        element.addEventListener(name, wrapper, false);
      } else {
        element.attachEvent("on" + name, wrapper);
      }

      return element;
    },

    stopObserving: function(element, eventName, handler) {
      element = $(element);
      var id = getEventID(element), name = getDOMEventName(eventName);

      if (!handler && eventName) {
        getWrappersForEventName(id, eventName).each(function(wrapper) {
          element.stopObserving(eventName, wrapper.handler);
        });
        return element;

      } else if (!eventName) {
        Object.keys(getCacheForID(id)).each(function(eventName) {
          element.stopObserving(eventName);
        });
        return element;
      }

      var wrapper = findWrapper(id, eventName, handler);
      if (!wrapper) return element;

      if (element.removeEventListener) {
        element.removeEventListener(name, wrapper, false);
      } else {
        element.detachEvent("on" + name, wrapper);
      }

      destroyWrapper(id, eventName, handler);

      return element;
    },

    fire: function(element, eventName, memo) {
      element = $(element);
      if (element == document && document.createEvent && !element.dispatchEvent)
        element = document.documentElement;

      var event;
      if (document.createEvent) {
        event = document.createEvent("HTMLEvents");
        event.initEvent("dataavailable", true, true);
      } else {
        event = document.createEventObject();
        event.eventType = "ondataavailable";
      }

      event.eventName = eventName;
      event.memo = memo || { };

      if (document.createEvent) {
        element.dispatchEvent(event);
      } else {
        element.fireEvent(event.eventType, event);
      }

      return Event.extend(event);
    }
  };
})());

Object.extend(Event, Event.Methods);

Element.addMethods({
  fire:          Event.fire,
  observe:       Event.observe,
  stopObserving: Event.stopObserving
});

Object.extend(document, {
  fire:          Element.Methods.fire.methodize(),
  observe:       Element.Methods.observe.methodize(),
  stopObserving: Element.Methods.stopObserving.methodize(),
  loaded:        false
});

(function() {
  /* Support for the DOMContentLoaded event is based on work by Dan Webb,
     Matthias Miller, Dean Edwards and John Resig. */

  var timer;

  function fireContentLoadedEvent() {
    if (document.loaded) return;
    if (timer) window.clearInterval(timer);
    document.fire("dom:loaded");
    document.loaded = true;
  }

  if (document.addEventListener) {
    if (Prototype.Browser.WebKit) {
      timer = window.setInterval(function() {
        if (/loaded|complete/.test(document.readyState))
          fireContentLoadedEvent();
      }, 0);

      Event.observe(window, "load", fireContentLoadedEvent);

    } else {
      document.addEventListener("DOMContentLoaded",
        fireContentLoadedEvent, false);
    }

  } else {
    document.write("<script id=__onDOMContentLoaded defer src=//:><\/script>");
    $("__onDOMContentLoaded").onreadystatechange = function() {
      if (this.readyState == "complete") {
        this.onreadystatechange = null;
        fireContentLoadedEvent();
      }
    };
  }
})();
/*------------------------------- DEPRECATED -------------------------------*/

Hash.toQueryString = Object.toQueryString;

var Toggle = { display: Element.toggle };

Element.Methods.childOf = Element.Methods.descendantOf;

var Insertion = {
  Before: function(element, content) {
    return Element.insert(element, {before:content});
  },

  Top: function(element, content) {
    return Element.insert(element, {top:content});
  },

  Bottom: function(element, content) {
    return Element.insert(element, {bottom:content});
  },

  After: function(element, content) {
    return Element.insert(element, {after:content});
  }
};

var $continue = new Error('"throw $continue" is deprecated, use "return" instead');

// This should be moved to script.aculo.us; notice the deprecated methods
// further below, that map to the newer Element methods.
var Position = {
  // set to true if needed, warning: firefox performance problems
  // NOT neeeded for page scrolling, only if draggable contained in
  // scrollable elements
  includeScrollOffsets: false,

  // must be called before calling withinIncludingScrolloffset, every time the
  // page is scrolled
  prepare: function() {
    this.deltaX =  window.pageXOffset
                || document.documentElement.scrollLeft
                || document.body.scrollLeft
                || 0;
    this.deltaY =  window.pageYOffset
                || document.documentElement.scrollTop
                || document.body.scrollTop
                || 0;
  },

  // caches x/y coordinate pair to use with overlap
  within: function(element, x, y) {
    if (this.includeScrollOffsets)
      return this.withinIncludingScrolloffsets(element, x, y);
    this.xcomp = x;
    this.ycomp = y;
    this.offset = Element.cumulativeOffset(element);

    return (y >= this.offset[1] &&
            y <  this.offset[1] + element.offsetHeight &&
            x >= this.offset[0] &&
            x <  this.offset[0] + element.offsetWidth);
  },

  withinIncludingScrolloffsets: function(element, x, y) {
    var offsetcache = Element.cumulativeScrollOffset(element);

    this.xcomp = x + offsetcache[0] - this.deltaX;
    this.ycomp = y + offsetcache[1] - this.deltaY;
    this.offset = Element.cumulativeOffset(element);

    return (this.ycomp >= this.offset[1] &&
            this.ycomp <  this.offset[1] + element.offsetHeight &&
            this.xcomp >= this.offset[0] &&
            this.xcomp <  this.offset[0] + element.offsetWidth);
  },

  // within must be called directly before
  overlap: function(mode, element) {
    if (!mode) return 0;
    if (mode == 'vertical')
      return ((this.offset[1] + element.offsetHeight) - this.ycomp) /
        element.offsetHeight;
    if (mode == 'horizontal')
      return ((this.offset[0] + element.offsetWidth) - this.xcomp) /
        element.offsetWidth;
  },

  // Deprecation layer -- use newer Element methods now (1.5.2).

  cumulativeOffset: Element.Methods.cumulativeOffset,

  positionedOffset: Element.Methods.positionedOffset,

  absolutize: function(element) {
    Position.prepare();
    return Element.absolutize(element);
  },

  relativize: function(element) {
    Position.prepare();
    return Element.relativize(element);
  },

  realOffset: Element.Methods.cumulativeScrollOffset,

  offsetParent: Element.Methods.getOffsetParent,

  page: Element.Methods.viewportOffset,

  clone: function(source, target, options) {
    options = options || { };
    return Element.clonePosition(target, source, options);
  }
};

/*--------------------------------------------------------------------------*/

if (!document.getElementsByClassName) document.getElementsByClassName = function(instanceMethods){
  function iter(name) {
    return name.blank() ? null : "[contains(concat(' ', @class, ' '), ' " + name + " ')]";
  }

  instanceMethods.getElementsByClassName = Prototype.BrowserFeatures.XPath ?
  function(element, className) {
    className = className.toString().strip();
    var cond = /\s/.test(className) ? $w(className).map(iter).join('') : iter(className);
    return cond ? document._getElementsByXPath('.//*' + cond, element) : [];
  } : function(element, className) {
    className = className.toString().strip();
    var elements = [], classNames = (/\s/.test(className) ? $w(className) : null);
    if (!classNames && !className) return elements;

    var nodes = $(element).getElementsByTagName('*');
    className = ' ' + className + ' ';

    for (var i = 0, child, cn; child = nodes[i]; i++) {
      if (child.className && (cn = ' ' + child.className + ' ') && (cn.include(className) ||
          (classNames && classNames.all(function(name) {
            return !name.toString().blank() && cn.include(' ' + name + ' ');
          }))))
        elements.push(Element.extend(child));
    }
    return elements;
  };

  return function(className, parentElement) {
    return $(parentElement || document.body).getElementsByClassName(className);
  };
}(Element.Methods);

/*--------------------------------------------------------------------------*/

Element.ClassNames = Class.create();
Element.ClassNames.prototype = {
  initialize: function(element) {
    this.element = $(element);
  },

  _each: function(iterator) {
    this.element.className.split(/\s+/).select(function(name) {
      return name.length > 0;
    })._each(iterator);
  },

  set: function(className) {
    this.element.className = className;
  },

  add: function(classNameToAdd) {
    if (this.include(classNameToAdd)) return;
    this.set($A(this).concat(classNameToAdd).join(' '));
  },

  remove: function(classNameToRemove) {
    if (!this.include(classNameToRemove)) return;
    this.set($A(this).without(classNameToRemove).join(' '));
  },

  toString: function() {
    return $A(this).join(' ');
  }
};

Object.extend(Element.ClassNames.prototype, Enumerable);

/*--------------------------------------------------------------------------*/

Element.addMethods();// script.aculo.us scriptaculous.js v1.8.2, Tue Nov 18 18:30:58 +0100 2008

// Copyright (c) 2005-2008 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us)
//
// Permission is hereby granted, free of charge, to any person obtaining
// a copy of this software and associated documentation files (the
// "Software"), to deal in the Software without restriction, including
// without limitation the rights to use, copy, modify, merge, publish,
// distribute, sublicense, and/or sell copies of the Software, and to
// permit persons to whom the Software is furnished to do so, subject to
// the following conditions:
//
// The above copyright notice and this permission notice shall be
// included in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
//
// For details, see the script.aculo.us web site: http://script.aculo.us/

var Scriptaculous = {
  Version: '1.8.2',
  require: function(libraryName) {
    // inserting via DOM fails in Safari 2.0, so brute force approach
    document.write('<script type="text/javascript" src="'+libraryName+'"><\/script>');
  },
  REQUIRED_PROTOTYPE: '1.6.0.3',
  load: function() {
    function convertVersionString(versionString) {
      var v = versionString.replace(/_.*|\./g, '');
      v = parseInt(v + '0'.times(4-v.length));
      return versionString.indexOf('_') > -1 ? v-1 : v;
    }

    if((typeof Prototype=='undefined') ||
       (typeof Element == 'undefined') ||
       (typeof Element.Methods=='undefined') ||
       (convertVersionString(Prototype.Version) <
        convertVersionString(Scriptaculous.REQUIRED_PROTOTYPE)))
       throw("script.aculo.us requires the Prototype JavaScript framework >= " +
        Scriptaculous.REQUIRED_PROTOTYPE);

    var js = /scriptaculous\.js(\?.*)?$/;
    $$('head script[src]').findAll(function(s) {
      return s.src.match(js);
    }).each(function(s) {
      var path = s.src.replace(js, ''),
      includes = s.src.match(/\?.*load=([a-z,]*)/);
      (includes ? includes[1] : 'builder,effects,dragdrop,controls,slider,sound').split(',').each(
       function(include) { Scriptaculous.require(path+include+'.js') });
    });
  }
};

Scriptaculous.load();// script.aculo.us builder.js v1.8.2, Tue Nov 18 18:30:58 +0100 2008

// Copyright (c) 2005-2008 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us)
//
// script.aculo.us is freely distributable under the terms of an MIT-style license.
// For details, see the script.aculo.us web site: http://script.aculo.us/

var Builder = {
  NODEMAP: {
    AREA: 'map',
    CAPTION: 'table',
    COL: 'table',
    COLGROUP: 'table',
    LEGEND: 'fieldset',
    OPTGROUP: 'select',
    OPTION: 'select',
    PARAM: 'object',
    TBODY: 'table',
    TD: 'table',
    TFOOT: 'table',
    TH: 'table',
    THEAD: 'table',
    TR: 'table'
  },
  // note: For Firefox < 1.5, OPTION and OPTGROUP tags are currently broken,
  //       due to a Firefox bug
  node: function(elementName) {
    elementName = elementName.toUpperCase();

    // try innerHTML approach
    var parentTag = this.NODEMAP[elementName] || 'div';
    var parentElement = document.createElement(parentTag);
    try { // prevent IE "feature": http://dev.rubyonrails.org/ticket/2707
      parentElement.innerHTML = "<" + elementName + "></" + elementName + ">";
    } catch(e) {}
    var element = parentElement.firstChild || null;

    // see if browser added wrapping tags
    if(element && (element.tagName.toUpperCase() != elementName))
      element = element.getElementsByTagName(elementName)[0];

    // fallback to createElement approach
    if(!element) element = document.createElement(elementName);

    // abort if nothing could be created
    if(!element) return;

    // attributes (or text)
    if(arguments[1])
      if(this._isStringOrNumber(arguments[1]) ||
        (arguments[1] instanceof Array) ||
        arguments[1].tagName) {
          this._children(element, arguments[1]);
        } else {
          var attrs = this._attributes(arguments[1]);
          if(attrs.length) {
            try { // prevent IE "feature": http://dev.rubyonrails.org/ticket/2707
              parentElement.innerHTML = "<" +elementName + " " +
                attrs + "></" + elementName + ">";
            } catch(e) {}
            element = parentElement.firstChild || null;
            // workaround firefox 1.0.X bug
            if(!element) {
              element = document.createElement(elementName);
              for(attr in arguments[1])
                element[attr == 'class' ? 'className' : attr] = arguments[1][attr];
            }
            if(element.tagName.toUpperCase() != elementName)
              element = parentElement.getElementsByTagName(elementName)[0];
          }
        }

    // text, or array of children
    if(arguments[2])
      this._children(element, arguments[2]);

     return $(element);
  },
  _text: function(text) {
     return document.createTextNode(text);
  },

  ATTR_MAP: {
    'className': 'class',
    'htmlFor': 'for'
  },

  _attributes: function(attributes) {
    var attrs = [];
    for(attribute in attributes)
      attrs.push((attribute in this.ATTR_MAP ? this.ATTR_MAP[attribute] : attribute) +
          '="' + attributes[attribute].toString().escapeHTML().gsub(/"/,'&quot;') + '"');
    return attrs.join(" ");
  },
  _children: function(element, children) {
    if(children.tagName) {
      element.appendChild(children);
      return;
    }
    if(typeof children=='object') { // array can hold nodes and text
      children.flatten().each( function(e) {
        if(typeof e=='object')
          element.appendChild(e);
        else
          if(Builder._isStringOrNumber(e))
            element.appendChild(Builder._text(e));
      });
    } else
      if(Builder._isStringOrNumber(children))
        element.appendChild(Builder._text(children));
  },
  _isStringOrNumber: function(param) {
    return(typeof param=='string' || typeof param=='number');
  },
  build: function(html) {
    var element = this.node('div');
    $(element).update(html.strip());
    return element.down();
  },
  dump: function(scope) {
    if(typeof scope != 'object' && typeof scope != 'function') scope = window; //global scope

    var tags = ("A ABBR ACRONYM ADDRESS APPLET AREA B BASE BASEFONT BDO BIG BLOCKQUOTE BODY " +
      "BR BUTTON CAPTION CENTER CITE CODE COL COLGROUP DD DEL DFN DIR DIV DL DT EM FIELDSET " +
      "FONT FORM FRAME FRAMESET H1 H2 H3 H4 H5 H6 HEAD HR HTML I IFRAME IMG INPUT INS ISINDEX "+
      "KBD LABEL LEGEND LI LINK MAP MENU META NOFRAMES NOSCRIPT OBJECT OL OPTGROUP OPTION P "+
      "PARAM PRE Q S SAMP SCRIPT SELECT SMALL SPAN STRIKE STRONG STYLE SUB SUP TABLE TBODY TD "+
      "TEXTAREA TFOOT TH THEAD TITLE TR TT U UL VAR").split(/\s+/);

    tags.each( function(tag){
      scope[tag] = function() {
        return Builder.node.apply(Builder, [tag].concat($A(arguments)));
      };
    });
  }
};// script.aculo.us effects.js v1.8.2, Tue Nov 18 18:30:58 +0100 2008

// Copyright (c) 2005-2008 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us)
// Contributors:
//  Justin Palmer (http://encytemedia.com/)
//  Mark Pilgrim (http://diveintomark.org/)
//  Martin Bialasinki
//
// script.aculo.us is freely distributable under the terms of an MIT-style license.
// For details, see the script.aculo.us web site: http://script.aculo.us/

// converts rgb() and #xxx to #xxxxxx format,
// returns self (or first argument) if not convertable
String.prototype.parseColor = function() {
  var color = '#';
  if (this.slice(0,4) == 'rgb(') {
    var cols = this.slice(4,this.length-1).split(',');
    var i=0; do { color += parseInt(cols[i]).toColorPart() } while (++i<3);
  } else {
    if (this.slice(0,1) == '#') {
      if (this.length==4) for(var i=1;i<4;i++) color += (this.charAt(i) + this.charAt(i)).toLowerCase();
      if (this.length==7) color = this.toLowerCase();
    }
  }
  return (color.length==7 ? color : (arguments[0] || this));
};

/*--------------------------------------------------------------------------*/

Element.collectTextNodes = function(element) {
  return $A($(element).childNodes).collect( function(node) {
    return (node.nodeType==3 ? node.nodeValue :
      (node.hasChildNodes() ? Element.collectTextNodes(node) : ''));
  }).flatten().join('');
};

Element.collectTextNodesIgnoreClass = function(element, className) {
  return $A($(element).childNodes).collect( function(node) {
    return (node.nodeType==3 ? node.nodeValue :
      ((node.hasChildNodes() && !Element.hasClassName(node,className)) ?
        Element.collectTextNodesIgnoreClass(node, className) : ''));
  }).flatten().join('');
};

Element.setContentZoom = function(element, percent) {
  element = $(element);
  element.setStyle({fontSize: (percent/100) + 'em'});
  if (Prototype.Browser.WebKit) window.scrollBy(0,0);
  return element;
};

Element.getInlineOpacity = function(element){
  return $(element).style.opacity || '';
};

Element.forceRerendering = function(element) {
  try {
    element = $(element);
    var n = document.createTextNode(' ');
    element.appendChild(n);
    element.removeChild(n);
  } catch(e) { }
};

/*--------------------------------------------------------------------------*/

var Effect = {
  _elementDoesNotExistError: {
    name: 'ElementDoesNotExistError',
    message: 'The specified DOM element does not exist, but is required for this effect to operate'
  },
  Transitions: {
    linear: Prototype.K,
    sinoidal: function(pos) {
      return (-Math.cos(pos*Math.PI)/2) + .5;
    },
    reverse: function(pos) {
      return 1-pos;
    },
    flicker: function(pos) {
      var pos = ((-Math.cos(pos*Math.PI)/4) + .75) + Math.random()/4;
      return pos > 1 ? 1 : pos;
    },
    wobble: function(pos) {
      return (-Math.cos(pos*Math.PI*(9*pos))/2) + .5;
    },
    pulse: function(pos, pulses) {
      return (-Math.cos((pos*((pulses||5)-.5)*2)*Math.PI)/2) + .5;
    },
    spring: function(pos) {
      return 1 - (Math.cos(pos * 4.5 * Math.PI) * Math.exp(-pos * 6));
    },
    none: function(pos) {
      return 0;
    },
    full: function(pos) {
      return 1;
    }
  },
  DefaultOptions: {
    duration:   1.0,   // seconds
    fps:        100,   // 100= assume 66fps max.
    sync:       false, // true for combining
    from:       0.0,
    to:         1.0,
    delay:      0.0,
    queue:      'parallel'
  },
  tagifyText: function(element) {
    var tagifyStyle = 'position:relative';
    if (Prototype.Browser.IE) tagifyStyle += ';zoom:1';

    element = $(element);
    $A(element.childNodes).each( function(child) {
      if (child.nodeType==3) {
        child.nodeValue.toArray().each( function(character) {
          element.insertBefore(
            new Element('span', {style: tagifyStyle}).update(
              character == ' ' ? String.fromCharCode(160) : character),
              child);
        });
        Element.remove(child);
      }
    });
  },
  multiple: function(element, effect) {
    var elements;
    if (((typeof element == 'object') ||
        Object.isFunction(element)) &&
       (element.length))
      elements = element;
    else
      elements = $(element).childNodes;

    var options = Object.extend({
      speed: 0.1,
      delay: 0.0
    }, arguments[2] || { });
    var masterDelay = options.delay;

    $A(elements).each( function(element, index) {
      new effect(element, Object.extend(options, { delay: index * options.speed + masterDelay }));
    });
  },
  PAIRS: {
    'slide':  ['SlideDown','SlideUp'],
    'blind':  ['BlindDown','BlindUp'],
    'appear': ['Appear','Fade']
  },
  toggle: function(element, effect) {
    element = $(element);
    effect = (effect || 'appear').toLowerCase();
    var options = Object.extend({
      queue: { position:'end', scope:(element.id || 'global'), limit: 1 }
    }, arguments[2] || { });
    Effect[element.visible() ?
      Effect.PAIRS[effect][1] : Effect.PAIRS[effect][0]](element, options);
  }
};

Effect.DefaultOptions.transition = Effect.Transitions.sinoidal;

/* ------------- core effects ------------- */

Effect.ScopedQueue = Class.create(Enumerable, {
  initialize: function() {
    this.effects  = [];
    this.interval = null;
  },
  _each: function(iterator) {
    this.effects._each(iterator);
  },
  add: function(effect) {
    var timestamp = new Date().getTime();

    var position = Object.isString(effect.options.queue) ?
      effect.options.queue : effect.options.queue.position;

    switch(position) {
      case 'front':
        // move unstarted effects after this effect
        this.effects.findAll(function(e){ return e.state=='idle' }).each( function(e) {
            e.startOn  += effect.finishOn;
            e.finishOn += effect.finishOn;
          });
        break;
      case 'with-last':
        timestamp = this.effects.pluck('startOn').max() || timestamp;
        break;
      case 'end':
        // start effect after last queued effect has finished
        timestamp = this.effects.pluck('finishOn').max() || timestamp;
        break;
    }

    effect.startOn  += timestamp;
    effect.finishOn += timestamp;

    if (!effect.options.queue.limit || (this.effects.length < effect.options.queue.limit))
      this.effects.push(effect);

    if (!this.interval)
      this.interval = setInterval(this.loop.bind(this), 15);
  },
  remove: function(effect) {
    this.effects = this.effects.reject(function(e) { return e==effect });
    if (this.effects.length == 0) {
      clearInterval(this.interval);
      this.interval = null;
    }
  },
  loop: function() {
    var timePos = new Date().getTime();
    for(var i=0, len=this.effects.length;i<len;i++)
      this.effects[i] && this.effects[i].loop(timePos);
  }
});

Effect.Queues = {
  instances: $H(),
  get: function(queueName) {
    if (!Object.isString(queueName)) return queueName;

    return this.instances.get(queueName) ||
      this.instances.set(queueName, new Effect.ScopedQueue());
  }
};
Effect.Queue = Effect.Queues.get('global');

Effect.Base = Class.create({
  position: null,
  start: function(options) {
    function codeForEvent(options,eventName){
      return (
        (options[eventName+'Internal'] ? 'this.options.'+eventName+'Internal(this);' : '') +
        (options[eventName] ? 'this.options.'+eventName+'(this);' : '')
      );
    }
    if (options && options.transition === false) options.transition = Effect.Transitions.linear;
    this.options      = Object.extend(Object.extend({ },Effect.DefaultOptions), options || { });
    this.currentFrame = 0;
    this.state        = 'idle';
    this.startOn      = this.options.delay*1000;
    this.finishOn     = this.startOn+(this.options.duration*1000);
    this.fromToDelta  = this.options.to-this.options.from;
    this.totalTime    = this.finishOn-this.startOn;
    this.totalFrames  = this.options.fps*this.options.duration;

    this.render = (function() {
      function dispatch(effect, eventName) {
        if (effect.options[eventName + 'Internal'])
          effect.options[eventName + 'Internal'](effect);
        if (effect.options[eventName])
          effect.options[eventName](effect);
      }

      return function(pos) {
        if (this.state === "idle") {
          this.state = "running";
          dispatch(this, 'beforeSetup');
          if (this.setup) this.setup();
          dispatch(this, 'afterSetup');
        }
        if (this.state === "running") {
          pos = (this.options.transition(pos) * this.fromToDelta) + this.options.from;
          this.position = pos;
          dispatch(this, 'beforeUpdate');
          if (this.update) this.update(pos);
          dispatch(this, 'afterUpdate');
        }
      };
    })();

    this.event('beforeStart');
    if (!this.options.sync)
      Effect.Queues.get(Object.isString(this.options.queue) ?
        'global' : this.options.queue.scope).add(this);
  },
  loop: function(timePos) {
    if (timePos >= this.startOn) {
      if (timePos >= this.finishOn) {
        this.render(1.0);
        this.cancel();
        this.event('beforeFinish');
        if (this.finish) this.finish();
        this.event('afterFinish');
        return;
      }
      var pos   = (timePos - this.startOn) / this.totalTime,
          frame = (pos * this.totalFrames).round();
      if (frame > this.currentFrame) {
        this.render(pos);
        this.currentFrame = frame;
      }
    }
  },
  cancel: function() {
    if (!this.options.sync)
      Effect.Queues.get(Object.isString(this.options.queue) ?
        'global' : this.options.queue.scope).remove(this);
    this.state = 'finished';
  },
  event: function(eventName) {
    if (this.options[eventName + 'Internal']) this.options[eventName + 'Internal'](this);
    if (this.options[eventName]) this.options[eventName](this);
  },
  inspect: function() {
    var data = $H();
    for(property in this)
      if (!Object.isFunction(this[property])) data.set(property, this[property]);
    return '#<Effect:' + data.inspect() + ',options:' + $H(this.options).inspect() + '>';
  }
});

Effect.Parallel = Class.create(Effect.Base, {
  initialize: function(effects) {
    this.effects = effects || [];
    this.start(arguments[1]);
  },
  update: function(position) {
    this.effects.invoke('render', position);
  },
  finish: function(position) {
    this.effects.each( function(effect) {
      effect.render(1.0);
      effect.cancel();
      effect.event('beforeFinish');
      if (effect.finish) effect.finish(position);
      effect.event('afterFinish');
    });
  }
});

Effect.Tween = Class.create(Effect.Base, {
  initialize: function(object, from, to) {
    object = Object.isString(object) ? $(object) : object;
    var args = $A(arguments), method = args.last(),
      options = args.length == 5 ? args[3] : null;
    this.method = Object.isFunction(method) ? method.bind(object) :
      Object.isFunction(object[method]) ? object[method].bind(object) :
      function(value) { object[method] = value };
    this.start(Object.extend({ from: from, to: to }, options || { }));
  },
  update: function(position) {
    this.method(position);
  }
});

Effect.Event = Class.create(Effect.Base, {
  initialize: function() {
    this.start(Object.extend({ duration: 0 }, arguments[0] || { }));
  },
  update: Prototype.emptyFunction
});

Effect.Opacity = Class.create(Effect.Base, {
  initialize: function(element) {
    this.element = $(element);
    if (!this.element) throw(Effect._elementDoesNotExistError);
    // make this work on IE on elements without 'layout'
    if (Prototype.Browser.IE && (!this.element.currentStyle.hasLayout))
      this.element.setStyle({zoom: 1});
    var options = Object.extend({
      from: this.element.getOpacity() || 0.0,
      to:   1.0
    }, arguments[1] || { });
    this.start(options);
  },
  update: function(position) {
    this.element.setOpacity(position);
  }
});

Effect.Move = Class.create(Effect.Base, {
  initialize: function(element) {
    this.element = $(element);
    if (!this.element) throw(Effect._elementDoesNotExistError);
    var options = Object.extend({
      x:    0,
      y:    0,
      mode: 'relative'
    }, arguments[1] || { });
    this.start(options);
  },
  setup: function() {
    this.element.makePositioned();
    this.originalLeft = parseFloat(this.element.getStyle('left') || '0');
    this.originalTop  = parseFloat(this.element.getStyle('top')  || '0');
    if (this.options.mode == 'absolute') {
      this.options.x = this.options.x - this.originalLeft;
      this.options.y = this.options.y - this.originalTop;
    }
  },
  update: function(position) {
    this.element.setStyle({
      left: (this.options.x  * position + this.originalLeft).round() + 'px',
      top:  (this.options.y  * position + this.originalTop).round()  + 'px'
    });
  }
});

// for backwards compatibility
Effect.MoveBy = function(element, toTop, toLeft) {
  return new Effect.Move(element,
    Object.extend({ x: toLeft, y: toTop }, arguments[3] || { }));
};

Effect.Scale = Class.create(Effect.Base, {
  initialize: function(element, percent) {
    this.element = $(element);
    if (!this.element) throw(Effect._elementDoesNotExistError);
    var options = Object.extend({
      scaleX: true,
      scaleY: true,
      scaleContent: true,
      scaleFromCenter: false,
      scaleMode: 'box',        // 'box' or 'contents' or { } with provided values
      scaleFrom: 100.0,
      scaleTo:   percent
    }, arguments[2] || { });
    this.start(options);
  },
  setup: function() {
    this.restoreAfterFinish = this.options.restoreAfterFinish || false;
    this.elementPositioning = this.element.getStyle('position');

    this.originalStyle = { };
    ['top','left','width','height','fontSize'].each( function(k) {
      this.originalStyle[k] = this.element.style[k];
    }.bind(this));

    this.originalTop  = this.element.offsetTop;
    this.originalLeft = this.element.offsetLeft;

    var fontSize = this.element.getStyle('font-size') || '100%';
    ['em','px','%','pt'].each( function(fontSizeType) {
      if (fontSize.indexOf(fontSizeType)>0) {
        this.fontSize     = parseFloat(fontSize);
        this.fontSizeType = fontSizeType;
      }
    }.bind(this));

    this.factor = (this.options.scaleTo - this.options.scaleFrom)/100;

    this.dims = null;
    if (this.options.scaleMode=='box')
      this.dims = [this.element.offsetHeight, this.element.offsetWidth];
    if (/^content/.test(this.options.scaleMode))
      this.dims = [this.element.scrollHeight, this.element.scrollWidth];
    if (!this.dims)
      this.dims = [this.options.scaleMode.originalHeight,
                   this.options.scaleMode.originalWidth];
  },
  update: function(position) {
    var currentScale = (this.options.scaleFrom/100.0) + (this.factor * position);
    if (this.options.scaleContent && this.fontSize)
      this.element.setStyle({fontSize: this.fontSize * currentScale + this.fontSizeType });
    this.setDimensions(this.dims[0] * currentScale, this.dims[1] * currentScale);
  },
  finish: function(position) {
    if (this.restoreAfterFinish) this.element.setStyle(this.originalStyle);
  },
  setDimensions: function(height, width) {
    var d = { };
    if (this.options.scaleX) d.width = width.round() + 'px';
    if (this.options.scaleY) d.height = height.round() + 'px';
    if (this.options.scaleFromCenter) {
      var topd  = (height - this.dims[0])/2;
      var leftd = (width  - this.dims[1])/2;
      if (this.elementPositioning == 'absolute') {
        if (this.options.scaleY) d.top = this.originalTop-topd + 'px';
        if (this.options.scaleX) d.left = this.originalLeft-leftd + 'px';
      } else {
        if (this.options.scaleY) d.top = -topd + 'px';
        if (this.options.scaleX) d.left = -leftd + 'px';
      }
    }
    this.element.setStyle(d);
  }
});

Effect.Highlight = Class.create(Effect.Base, {
  initialize: function(element) {
    this.element = $(element);
    if (!this.element) throw(Effect._elementDoesNotExistError);
    var options = Object.extend({ startcolor: '#ffff99' }, arguments[1] || { });
    this.start(options);
  },
  setup: function() {
    // Prevent executing on elements not in the layout flow
    if (this.element.getStyle('display')=='none') { this.cancel(); return; }
    // Disable background image during the effect
    this.oldStyle = { };
    if (!this.options.keepBackgroundImage) {
      this.oldStyle.backgroundImage = this.element.getStyle('background-image');
      this.element.setStyle({backgroundImage: 'none'});
    }
    if (!this.options.endcolor)
      this.options.endcolor = this.element.getStyle('background-color').parseColor('#ffffff');
    if (!this.options.restorecolor)
      this.options.restorecolor = this.element.getStyle('background-color');
    // init color calculations
    this._base  = $R(0,2).map(function(i){ return parseInt(this.options.startcolor.slice(i*2+1,i*2+3),16) }.bind(this));
    this._delta = $R(0,2).map(function(i){ return parseInt(this.options.endcolor.slice(i*2+1,i*2+3),16)-this._base[i] }.bind(this));
  },
  update: function(position) {
    this.element.setStyle({backgroundColor: $R(0,2).inject('#',function(m,v,i){
      return m+((this._base[i]+(this._delta[i]*position)).round().toColorPart()); }.bind(this)) });
  },
  finish: function() {
    this.element.setStyle(Object.extend(this.oldStyle, {
      backgroundColor: this.options.restorecolor
    }));
  }
});

Effect.ScrollTo = function(element) {
  var options = arguments[1] || { },
  scrollOffsets = document.viewport.getScrollOffsets(),
  elementOffsets = $(element).cumulativeOffset();

  if (options.offset) elementOffsets[1] += options.offset;

  return new Effect.Tween(null,
    scrollOffsets.top,
    elementOffsets[1],
    options,
    function(p){ scrollTo(scrollOffsets.left, p.round()); }
  );
};

/* ------------- combination effects ------------- */

Effect.Fade = function(element) {
  element = $(element);
  var oldOpacity = element.getInlineOpacity();
  var options = Object.extend({
    from: element.getOpacity() || 1.0,
    to:   0.0,
    afterFinishInternal: function(effect) {
      if (effect.options.to!=0) return;
      effect.element.hide().setStyle({opacity: oldOpacity});
    }
  }, arguments[1] || { });
  return new Effect.Opacity(element,options);
};

Effect.Appear = function(element) {
  element = $(element);
  var options = Object.extend({
  from: (element.getStyle('display') == 'none' ? 0.0 : element.getOpacity() || 0.0),
  to:   1.0,
  // force Safari to render floated elements properly
  afterFinishInternal: function(effect) {
    effect.element.forceRerendering();
  },
  beforeSetup: function(effect) {
    effect.element.setOpacity(effect.options.from).show();
  }}, arguments[1] || { });
  return new Effect.Opacity(element,options);
};

Effect.Puff = function(element) {
  element = $(element);
  var oldStyle = {
    opacity: element.getInlineOpacity(),
    position: element.getStyle('position'),
    top:  element.style.top,
    left: element.style.left,
    width: element.style.width,
    height: element.style.height
  };
  return new Effect.Parallel(
   [ new Effect.Scale(element, 200,
      { sync: true, scaleFromCenter: true, scaleContent: true, restoreAfterFinish: true }),
     new Effect.Opacity(element, { sync: true, to: 0.0 } ) ],
     Object.extend({ duration: 1.0,
      beforeSetupInternal: function(effect) {
        Position.absolutize(effect.effects[0].element);
      },
      afterFinishInternal: function(effect) {
         effect.effects[0].element.hide().setStyle(oldStyle); }
     }, arguments[1] || { })
   );
};

Effect.BlindUp = function(element) {
  element = $(element);
  element.makeClipping();
  return new Effect.Scale(element, 0,
    Object.extend({ scaleContent: false,
      scaleX: false,
      restoreAfterFinish: true,
      afterFinishInternal: function(effect) {
        effect.element.hide().undoClipping();
      }
    }, arguments[1] || { })
  );
};

Effect.BlindDown = function(element) {
  element = $(element);
  var elementDimensions = element.getDimensions();
  return new Effect.Scale(element, 100, Object.extend({
    scaleContent: false,
    scaleX: false,
    scaleFrom: 0,
    scaleMode: {originalHeight: elementDimensions.height, originalWidth: elementDimensions.width},
    restoreAfterFinish: true,
    afterSetup: function(effect) {
      effect.element.makeClipping().setStyle({height: '0px'}).show();
    },
    afterFinishInternal: function(effect) {
      effect.element.undoClipping();
    }
  }, arguments[1] || { }));
};

Effect.SwitchOff = function(element) {
  element = $(element);
  var oldOpacity = element.getInlineOpacity();
  return new Effect.Appear(element, Object.extend({
    duration: 0.4,
    from: 0,
    transition: Effect.Transitions.flicker,
    afterFinishInternal: function(effect) {
      new Effect.Scale(effect.element, 1, {
        duration: 0.3, scaleFromCenter: true,
        scaleX: false, scaleContent: false, restoreAfterFinish: true,
        beforeSetup: function(effect) {
          effect.element.makePositioned().makeClipping();
        },
        afterFinishInternal: function(effect) {
          effect.element.hide().undoClipping().undoPositioned().setStyle({opacity: oldOpacity});
        }
      });
    }
  }, arguments[1] || { }));
};

Effect.DropOut = function(element) {
  element = $(element);
  var oldStyle = {
    top: element.getStyle('top'),
    left: element.getStyle('left'),
    opacity: element.getInlineOpacity() };
  return new Effect.Parallel(
    [ new Effect.Move(element, {x: 0, y: 100, sync: true }),
      new Effect.Opacity(element, { sync: true, to: 0.0 }) ],
    Object.extend(
      { duration: 0.5,
        beforeSetup: function(effect) {
          effect.effects[0].element.makePositioned();
        },
        afterFinishInternal: function(effect) {
          effect.effects[0].element.hide().undoPositioned().setStyle(oldStyle);
        }
      }, arguments[1] || { }));
};

Effect.Shake = function(element) {
  element = $(element);
  var options = Object.extend({
    distance: 20,
    duration: 0.5
  }, arguments[1] || {});
  var distance = parseFloat(options.distance);
  var split = parseFloat(options.duration) / 10.0;
  var oldStyle = {
    top: element.getStyle('top'),
    left: element.getStyle('left') };
    return new Effect.Move(element,
      { x:  distance, y: 0, duration: split, afterFinishInternal: function(effect) {
    new Effect.Move(effect.element,
      { x: -distance*2, y: 0, duration: split*2,  afterFinishInternal: function(effect) {
    new Effect.Move(effect.element,
      { x:  distance*2, y: 0, duration: split*2,  afterFinishInternal: function(effect) {
    new Effect.Move(effect.element,
      { x: -distance*2, y: 0, duration: split*2,  afterFinishInternal: function(effect) {
    new Effect.Move(effect.element,
      { x:  distance*2, y: 0, duration: split*2,  afterFinishInternal: function(effect) {
    new Effect.Move(effect.element,
      { x: -distance, y: 0, duration: split, afterFinishInternal: function(effect) {
        effect.element.undoPositioned().setStyle(oldStyle);
  }}); }}); }}); }}); }}); }});
};

Effect.SlideDown = function(element) {
  element = $(element).cleanWhitespace();
  // SlideDown need to have the content of the element wrapped in a container element with fixed height!
  var oldInnerBottom = element.down().getStyle('bottom');
  var elementDimensions = element.getDimensions();
  return new Effect.Scale(element, 100, Object.extend({
    scaleContent: false,
    scaleX: false,
    scaleFrom: window.opera ? 0 : 1,
    scaleMode: {originalHeight: elementDimensions.height, originalWidth: elementDimensions.width},
    restoreAfterFinish: true,
    afterSetup: function(effect) {
      effect.element.makePositioned();
      effect.element.down().makePositioned();
      if (window.opera) effect.element.setStyle({top: ''});
      effect.element.makeClipping().setStyle({height: '0px'}).show();
    },
    afterUpdateInternal: function(effect) {
      effect.element.down().setStyle({bottom:
        (effect.dims[0] - effect.element.clientHeight) + 'px' });
    },
    afterFinishInternal: function(effect) {
      effect.element.undoClipping().undoPositioned();
      effect.element.down().undoPositioned().setStyle({bottom: oldInnerBottom}); }
    }, arguments[1] || { })
  );
};

Effect.SlideUp = function(element) {
  element = $(element).cleanWhitespace();
  var oldInnerBottom = element.down().getStyle('bottom');
  var elementDimensions = element.getDimensions();
  return new Effect.Scale(element, window.opera ? 0 : 1,
   Object.extend({ scaleContent: false,
    scaleX: false,
    scaleMode: 'box',
    scaleFrom: 100,
    scaleMode: {originalHeight: elementDimensions.height, originalWidth: elementDimensions.width},
    restoreAfterFinish: true,
    afterSetup: function(effect) {
      effect.element.makePositioned();
      effect.element.down().makePositioned();
      if (window.opera) effect.element.setStyle({top: ''});
      effect.element.makeClipping().show();
    },
    afterUpdateInternal: function(effect) {
      effect.element.down().setStyle({bottom:
        (effect.dims[0] - effect.element.clientHeight) + 'px' });
    },
    afterFinishInternal: function(effect) {
      effect.element.hide().undoClipping().undoPositioned();
      effect.element.down().undoPositioned().setStyle({bottom: oldInnerBottom});
    }
   }, arguments[1] || { })
  );
};

// Bug in opera makes the TD containing this element expand for a instance after finish
Effect.Squish = function(element) {
  return new Effect.Scale(element, window.opera ? 1 : 0, {
    restoreAfterFinish: true,
    beforeSetup: function(effect) {
      effect.element.makeClipping();
    },
    afterFinishInternal: function(effect) {
      effect.element.hide().undoClipping();
    }
  });
};

Effect.Grow = function(element) {
  element = $(element);
  var options = Object.extend({
    direction: 'center',
    moveTransition: Effect.Transitions.sinoidal,
    scaleTransition: Effect.Transitions.sinoidal,
    opacityTransition: Effect.Transitions.full
  }, arguments[1] || { });
  var oldStyle = {
    top: element.style.top,
    left: element.style.left,
    height: element.style.height,
    width: element.style.width,
    opacity: element.getInlineOpacity() };

  var dims = element.getDimensions();
  var initialMoveX, initialMoveY;
  var moveX, moveY;

  switch (options.direction) {
    case 'top-left':
      initialMoveX = initialMoveY = moveX = moveY = 0;
      break;
    case 'top-right':
      initialMoveX = dims.width;
      initialMoveY = moveY = 0;
      moveX = -dims.width;
      break;
    case 'bottom-left':
      initialMoveX = moveX = 0;
      initialMoveY = dims.height;
      moveY = -dims.height;
      break;
    case 'bottom-right':
      initialMoveX = dims.width;
      initialMoveY = dims.height;
      moveX = -dims.width;
      moveY = -dims.height;
      break;
    case 'center':
      initialMoveX = dims.width / 2;
      initialMoveY = dims.height / 2;
      moveX = -dims.width / 2;
      moveY = -dims.height / 2;
      break;
  }

  return new Effect.Move(element, {
    x: initialMoveX,
    y: initialMoveY,
    duration: 0.01,
    beforeSetup: function(effect) {
      effect.element.hide().makeClipping().makePositioned();
    },
    afterFinishInternal: function(effect) {
      new Effect.Parallel(
        [ new Effect.Opacity(effect.element, { sync: true, to: 1.0, from: 0.0, transition: options.opacityTransition }),
          new Effect.Move(effect.element, { x: moveX, y: moveY, sync: true, transition: options.moveTransition }),
          new Effect.Scale(effect.element, 100, {
            scaleMode: { originalHeight: dims.height, originalWidth: dims.width },
            sync: true, scaleFrom: window.opera ? 1 : 0, transition: options.scaleTransition, restoreAfterFinish: true})
        ], Object.extend({
             beforeSetup: function(effect) {
               effect.effects[0].element.setStyle({height: '0px'}).show();
             },
             afterFinishInternal: function(effect) {
               effect.effects[0].element.undoClipping().undoPositioned().setStyle(oldStyle);
             }
           }, options)
      );
    }
  });
};

Effect.Shrink = function(element) {
  element = $(element);
  var options = Object.extend({
    direction: 'center',
    moveTransition: Effect.Transitions.sinoidal,
    scaleTransition: Effect.Transitions.sinoidal,
    opacityTransition: Effect.Transitions.none
  }, arguments[1] || { });
  var oldStyle = {
    top: element.style.top,
    left: element.style.left,
    height: element.style.height,
    width: element.style.width,
    opacity: element.getInlineOpacity() };

  var dims = element.getDimensions();
  var moveX, moveY;

  switch (options.direction) {
    case 'top-left':
      moveX = moveY = 0;
      break;
    case 'top-right':
      moveX = dims.width;
      moveY = 0;
      break;
    case 'bottom-left':
      moveX = 0;
      moveY = dims.height;
      break;
    case 'bottom-right':
      moveX = dims.width;
      moveY = dims.height;
      break;
    case 'center':
      moveX = dims.width / 2;
      moveY = dims.height / 2;
      break;
  }

  return new Effect.Parallel(
    [ new Effect.Opacity(element, { sync: true, to: 0.0, from: 1.0, transition: options.opacityTransition }),
      new Effect.Scale(element, window.opera ? 1 : 0, { sync: true, transition: options.scaleTransition, restoreAfterFinish: true}),
      new Effect.Move(element, { x: moveX, y: moveY, sync: true, transition: options.moveTransition })
    ], Object.extend({
         beforeStartInternal: function(effect) {
           effect.effects[0].element.makePositioned().makeClipping();
         },
         afterFinishInternal: function(effect) {
           effect.effects[0].element.hide().undoClipping().undoPositioned().setStyle(oldStyle); }
       }, options)
  );
};

Effect.Pulsate = function(element) {
  element = $(element);
  var options    = arguments[1] || { },
    oldOpacity = element.getInlineOpacity(),
    transition = options.transition || Effect.Transitions.linear,
    reverser   = function(pos){
      return 1 - transition((-Math.cos((pos*(options.pulses||5)*2)*Math.PI)/2) + .5);
    };

  return new Effect.Opacity(element,
    Object.extend(Object.extend({  duration: 2.0, from: 0,
      afterFinishInternal: function(effect) { effect.element.setStyle({opacity: oldOpacity}); }
    }, options), {transition: reverser}));
};

Effect.Fold = function(element) {
  element = $(element);
  var oldStyle = {
    top: element.style.top,
    left: element.style.left,
    width: element.style.width,
    height: element.style.height };
  element.makeClipping();
  return new Effect.Scale(element, 5, Object.extend({
    scaleContent: false,
    scaleX: false,
    afterFinishInternal: function(effect) {
    new Effect.Scale(element, 1, {
      scaleContent: false,
      scaleY: false,
      afterFinishInternal: function(effect) {
        effect.element.hide().undoClipping().setStyle(oldStyle);
      } });
  }}, arguments[1] || { }));
};

Effect.Morph = Class.create(Effect.Base, {
  initialize: function(element) {
    this.element = $(element);
    if (!this.element) throw(Effect._elementDoesNotExistError);
    var options = Object.extend({
      style: { }
    }, arguments[1] || { });

    if (!Object.isString(options.style)) this.style = $H(options.style);
    else {
      if (options.style.include(':'))
        this.style = options.style.parseStyle();
      else {
        this.element.addClassName(options.style);
        this.style = $H(this.element.getStyles());
        this.element.removeClassName(options.style);
        var css = this.element.getStyles();
        this.style = this.style.reject(function(style) {
          return style.value == css[style.key];
        });
        options.afterFinishInternal = function(effect) {
          effect.element.addClassName(effect.options.style);
          effect.transforms.each(function(transform) {
            effect.element.style[transform.style] = '';
          });
        };
      }
    }
    this.start(options);
  },

  setup: function(){
    function parseColor(color){
      if (!color || ['rgba(0, 0, 0, 0)','transparent'].include(color)) color = '#ffffff';
      color = color.parseColor();
      return $R(0,2).map(function(i){
        return parseInt( color.slice(i*2+1,i*2+3), 16 );
      });
    }
    this.transforms = this.style.map(function(pair){
      var property = pair[0], value = pair[1], unit = null;

      if (value.parseColor('#zzzzzz') != '#zzzzzz') {
        value = value.parseColor();
        unit  = 'color';
      } else if (property == 'opacity') {
        value = parseFloat(value);
        if (Prototype.Browser.IE && (!this.element.currentStyle.hasLayout))
          this.element.setStyle({zoom: 1});
      } else if (Element.CSS_LENGTH.test(value)) {
          var components = value.match(/^([\+\-]?[0-9\.]+)(.*)$/);
          value = parseFloat(components[1]);
          unit = (components.length == 3) ? components[2] : null;
      }

      var originalValue = this.element.getStyle(property);
      return {
        style: property.camelize(),
        originalValue: unit=='color' ? parseColor(originalValue) : parseFloat(originalValue || 0),
        targetValue: unit=='color' ? parseColor(value) : value,
        unit: unit
      };
    }.bind(this)).reject(function(transform){
      return (
        (transform.originalValue == transform.targetValue) ||
        (
          transform.unit != 'color' &&
          (isNaN(transform.originalValue) || isNaN(transform.targetValue))
        )
      );
    });
  },
  update: function(position) {
    var style = { }, transform, i = this.transforms.length;
    while(i--)
      style[(transform = this.transforms[i]).style] =
        transform.unit=='color' ? '#'+
          (Math.round(transform.originalValue[0]+
            (transform.targetValue[0]-transform.originalValue[0])*position)).toColorPart() +
          (Math.round(transform.originalValue[1]+
            (transform.targetValue[1]-transform.originalValue[1])*position)).toColorPart() +
          (Math.round(transform.originalValue[2]+
            (transform.targetValue[2]-transform.originalValue[2])*position)).toColorPart() :
        (transform.originalValue +
          (transform.targetValue - transform.originalValue) * position).toFixed(3) +
            (transform.unit === null ? '' : transform.unit);
    this.element.setStyle(style, true);
  }
});

Effect.Transform = Class.create({
  initialize: function(tracks){
    this.tracks  = [];
    this.options = arguments[1] || { };
    this.addTracks(tracks);
  },
  addTracks: function(tracks){
    tracks.each(function(track){
      track = $H(track);
      var data = track.values().first();
      this.tracks.push($H({
        ids:     track.keys().first(),
        effect:  Effect.Morph,
        options: { style: data }
      }));
    }.bind(this));
    return this;
  },
  play: function(){
    return new Effect.Parallel(
      this.tracks.map(function(track){
        var ids = track.get('ids'), effect = track.get('effect'), options = track.get('options');
        var elements = [$(ids) || $$(ids)].flatten();
        return elements.map(function(e){ return new effect(e, Object.extend({ sync:true }, options)) });
      }).flatten(),
      this.options
    );
  }
});

Element.CSS_PROPERTIES = $w(
  'backgroundColor backgroundPosition borderBottomColor borderBottomStyle ' +
  'borderBottomWidth borderLeftColor borderLeftStyle borderLeftWidth ' +
  'borderRightColor borderRightStyle borderRightWidth borderSpacing ' +
  'borderTopColor borderTopStyle borderTopWidth bottom clip color ' +
  'fontSize fontWeight height left letterSpacing lineHeight ' +
  'marginBottom marginLeft marginRight marginTop markerOffset maxHeight '+
  'maxWidth minHeight minWidth opacity outlineColor outlineOffset ' +
  'outlineWidth paddingBottom paddingLeft paddingRight paddingTop ' +
  'right textIndent top width wordSpacing zIndex');

Element.CSS_LENGTH = /^(([\+\-]?[0-9\.]+)(em|ex|px|in|cm|mm|pt|pc|\%))|0$/;

String.__parseStyleElement = document.createElement('div');
String.prototype.parseStyle = function(){
  var style, styleRules = $H();
  if (Prototype.Browser.WebKit)
    style = new Element('div',{style:this}).style;
  else {
    String.__parseStyleElement.innerHTML = '<div style="' + this + '"></div>';
    style = String.__parseStyleElement.childNodes[0].style;
  }

  Element.CSS_PROPERTIES.each(function(property){
    if (style[property]) styleRules.set(property, style[property]);
  });

  if (Prototype.Browser.IE && this.include('opacity'))
    styleRules.set('opacity', this.match(/opacity:\s*((?:0|1)?(?:\.\d*)?)/)[1]);

  return styleRules;
};

if (document.defaultView && document.defaultView.getComputedStyle) {
  Element.getStyles = function(element) {
    var css = document.defaultView.getComputedStyle($(element), null);
    return Element.CSS_PROPERTIES.inject({ }, function(styles, property) {
      styles[property] = css[property];
      return styles;
    });
  };
} else {
  Element.getStyles = function(element) {
    element = $(element);
    var css = element.currentStyle, styles;
    styles = Element.CSS_PROPERTIES.inject({ }, function(results, property) {
      results[property] = css[property];
      return results;
    });
    if (!styles.opacity) styles.opacity = element.getOpacity();
    return styles;
  };
}

Effect.Methods = {
  morph: function(element, style) {
    element = $(element);
    new Effect.Morph(element, Object.extend({ style: style }, arguments[2] || { }));
    return element;
  },
  visualEffect: function(element, effect, options) {
    element = $(element);
    var s = effect.dasherize().camelize(), klass = s.charAt(0).toUpperCase() + s.substring(1);
    new Effect[klass](element, options);
    return element;
  },
  highlight: function(element, options) {
    element = $(element);
    new Effect.Highlight(element, options);
    return element;
  }
};

$w('fade appear grow shrink fold blindUp blindDown slideUp slideDown '+
  'pulsate shake puff squish switchOff dropOut').each(
  function(effect) {
    Effect.Methods[effect] = function(element, options){
      element = $(element);
      Effect[effect.charAt(0).toUpperCase() + effect.substring(1)](element, options);
      return element;
    };
  }
);

$w('getInlineOpacity forceRerendering setContentZoom collectTextNodes collectTextNodesIgnoreClass getStyles').each(
  function(f) { Effect.Methods[f] = Element[f]; }
);

Element.addMethods(Effect.Methods);// script.aculo.us dragdrop.js v1.8.2, Tue Nov 18 18:30:58 +0100 2008

// Copyright (c) 2005-2008 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us)
//           (c) 2005-2008 Sammi Williams (http://www.oriontransfer.co.nz, sammi@oriontransfer.co.nz)
//
// script.aculo.us is freely distributable under the terms of an MIT-style license.
// For details, see the script.aculo.us web site: http://script.aculo.us/

if(Object.isUndefined(Effect))
  throw("dragdrop.js requires including script.aculo.us' effects.js library");

var Droppables = {
  drops: [],

  remove: function(element) {
    this.drops = this.drops.reject(function(d) { return d.element==$(element) });
  },

  add: function(element) {
    element = $(element);
    var options = Object.extend({
      greedy:     true,
      hoverclass: null,
      tree:       false
    }, arguments[1] || { });

    // cache containers
    if(options.containment) {
      options._containers = [];
      var containment = options.containment;
      if(Object.isArray(containment)) {
        containment.each( function(c) { options._containers.push($(c)) });
      } else {
        options._containers.push($(containment));
      }
    }

    if(options.accept) options.accept = [options.accept].flatten();

    Element.makePositioned(element); // fix IE
    options.element = element;

    this.drops.push(options);
  },

  findDeepestChild: function(drops) {
    deepest = drops[0];

    for (i = 1; i < drops.length; ++i)
      if (Element.isParent(drops[i].element, deepest.element))
        deepest = drops[i];

    return deepest;
  },

  isContained: function(element, drop) {
    var containmentNode;
    if(drop.tree) {
      containmentNode = element.treeNode;
    } else {
      containmentNode = element.parentNode;
    }
    return drop._containers.detect(function(c) { return containmentNode == c });
  },

  isAffected: function(point, element, drop) {
    return (
      (drop.element!=element) &&
      ((!drop._containers) ||
        this.isContained(element, drop)) &&
      ((!drop.accept) ||
        (Element.classNames(element).detect(
          function(v) { return drop.accept.include(v) } ) )) &&
      Position.within(drop.element, point[0], point[1]) );
  },

  deactivate: function(drop) {
    if(drop.hoverclass)
      Element.removeClassName(drop.element, drop.hoverclass);
    this.last_active = null;
  },

  activate: function(drop) {
    if(drop.hoverclass)
      Element.addClassName(drop.element, drop.hoverclass);
    this.last_active = drop;
  },

  show: function(point, element) {
    if(!this.drops.length) return;
    var drop, affected = [];

    this.drops.each( function(drop) {
      if(Droppables.isAffected(point, element, drop))
        affected.push(drop);
    });

    if(affected.length>0)
      drop = Droppables.findDeepestChild(affected);

    if(this.last_active && this.last_active != drop) this.deactivate(this.last_active);
    if (drop) {
      Position.within(drop.element, point[0], point[1]);
      if(drop.onHover)
        drop.onHover(element, drop.element, Position.overlap(drop.overlap, drop.element));

      if (drop != this.last_active) Droppables.activate(drop);
    }
  },

  fire: function(event, element) {
    if(!this.last_active) return;
    Position.prepare();

    if (this.isAffected([Event.pointerX(event), Event.pointerY(event)], element, this.last_active))
      if (this.last_active.onDrop) {
        this.last_active.onDrop(element, this.last_active.element, event);
        return true;
      }
  },

  reset: function() {
    if(this.last_active)
      this.deactivate(this.last_active);
  }
};

var Draggables = {
  drags: [],
  observers: [],

  register: function(draggable) {
    if(this.drags.length == 0) {
      this.eventMouseUp   = this.endDrag.bindAsEventListener(this);
      this.eventMouseMove = this.updateDrag.bindAsEventListener(this);
      this.eventKeypress  = this.keyPress.bindAsEventListener(this);

      Event.observe(document, "mouseup", this.eventMouseUp);
      Event.observe(document, "mousemove", this.eventMouseMove);
      Event.observe(document, "keypress", this.eventKeypress);
    }
    this.drags.push(draggable);
  },

  unregister: function(draggable) {
    this.drags = this.drags.reject(function(d) { return d==draggable });
    if(this.drags.length == 0) {
      Event.stopObserving(document, "mouseup", this.eventMouseUp);
      Event.stopObserving(document, "mousemove", this.eventMouseMove);
      Event.stopObserving(document, "keypress", this.eventKeypress);
    }
  },

  activate: function(draggable) {
    if(draggable.options.delay) {
      this._timeout = setTimeout(function() {
        Draggables._timeout = null;
        window.focus();
        Draggables.activeDraggable = draggable;
      }.bind(this), draggable.options.delay);
    } else {
      window.focus(); // allows keypress events if window isn't currently focused, fails for Safari
      this.activeDraggable = draggable;
    }
  },

  deactivate: function() {
    this.activeDraggable = null;
  },

  updateDrag: function(event) {
    if(!this.activeDraggable) return;
    var pointer = [Event.pointerX(event), Event.pointerY(event)];
    // Mozilla-based browsers fire successive mousemove events with
    // the same coordinates, prevent needless redrawing (moz bug?)
    if(this._lastPointer && (this._lastPointer.inspect() == pointer.inspect())) return;
    this._lastPointer = pointer;

    this.activeDraggable.updateDrag(event, pointer);
  },

  endDrag: function(event) {
    if(this._timeout) {
      clearTimeout(this._timeout);
      this._timeout = null;
    }
    if(!this.activeDraggable) return;
    this._lastPointer = null;
    this.activeDraggable.endDrag(event);
    this.activeDraggable = null;
  },

  keyPress: function(event) {
    if(this.activeDraggable)
      this.activeDraggable.keyPress(event);
  },

  addObserver: function(observer) {
    this.observers.push(observer);
    this._cacheObserverCallbacks();
  },

  removeObserver: function(element) {  // element instead of observer fixes mem leaks
    this.observers = this.observers.reject( function(o) { return o.element==element });
    this._cacheObserverCallbacks();
  },

  notify: function(eventName, draggable, event) {  // 'onStart', 'onEnd', 'onDrag'
    if(this[eventName+'Count'] > 0)
      this.observers.each( function(o) {
        if(o[eventName]) o[eventName](eventName, draggable, event);
      });
    if(draggable.options[eventName]) draggable.options[eventName](draggable, event);
  },

  _cacheObserverCallbacks: function() {
    ['onStart','onEnd','onDrag'].each( function(eventName) {
      Draggables[eventName+'Count'] = Draggables.observers.select(
        function(o) { return o[eventName]; }
      ).length;
    });
  }
};

/*--------------------------------------------------------------------------*/

var Draggable = Class.create({
  initialize: function(element) {
    var defaults = {
      handle: false,
      reverteffect: function(element, top_offset, left_offset) {
        var dur = Math.sqrt(Math.abs(top_offset^2)+Math.abs(left_offset^2))*0.02;
        new Effect.Move(element, { x: -left_offset, y: -top_offset, duration: dur,
          queue: {scope:'_draggable', position:'end'}
        });
      },
      endeffect: function(element) {
        var toOpacity = Object.isNumber(element._opacity) ? element._opacity : 1.0;
        new Effect.Opacity(element, {duration:0.2, from:0.7, to:toOpacity,
          queue: {scope:'_draggable', position:'end'},
          afterFinish: function(){
            Draggable._dragging[element] = false
          }
        });
      },
      zindex: 1000,
      revert: false,
      quiet: false,
      scroll: false,
      scrollSensitivity: 20,
      scrollSpeed: 15,
      snap: false,  // false, or xy or [x,y] or function(x,y){ return [x,y] }
      delay: 0
    };

    if(!arguments[1] || Object.isUndefined(arguments[1].endeffect))
      Object.extend(defaults, {
        starteffect: function(element) {
          element._opacity = Element.getOpacity(element);
          Draggable._dragging[element] = true;
          new Effect.Opacity(element, {duration:0.2, from:element._opacity, to:0.7});
        }
      });

    var options = Object.extend(defaults, arguments[1] || { });

    this.element = $(element);

    if(options.handle && Object.isString(options.handle))
      this.handle = this.element.down('.'+options.handle, 0);

    if(!this.handle) this.handle = $(options.handle);
    if(!this.handle) this.handle = this.element;

    if(options.scroll && !options.scroll.scrollTo && !options.scroll.outerHTML) {
      options.scroll = $(options.scroll);
      this._isScrollChild = Element.childOf(this.element, options.scroll);
    }

    Element.makePositioned(this.element); // fix IE

    this.options  = options;
    this.dragging = false;

    this.eventMouseDown = this.initDrag.bindAsEventListener(this);
    Event.observe(this.handle, "mousedown", this.eventMouseDown);

    Draggables.register(this);
  },

  destroy: function() {
    Event.stopObserving(this.handle, "mousedown", this.eventMouseDown);
    Draggables.unregister(this);
  },

  currentDelta: function() {
    return([
      parseInt(Element.getStyle(this.element,'left') || '0'),
      parseInt(Element.getStyle(this.element,'top') || '0')]);
  },

  initDrag: function(event) {
    if(!Object.isUndefined(Draggable._dragging[this.element]) &&
      Draggable._dragging[this.element]) return;
    if(Event.isLeftClick(event)) {
      // abort on form elements, fixes a Firefox issue
      var src = Event.element(event);
      if((tag_name = src.tagName.toUpperCase()) && (
        tag_name=='INPUT' ||
        tag_name=='SELECT' ||
        tag_name=='OPTION' ||
        tag_name=='BUTTON' ||
        tag_name=='TEXTAREA')) return;

      var pointer = [Event.pointerX(event), Event.pointerY(event)];
      var pos     = Position.cumulativeOffset(this.element);
      this.offset = [0,1].map( function(i) { return (pointer[i] - pos[i]) });

      Draggables.activate(this);
      Event.stop(event);
    }
  },

  startDrag: function(event) {
    this.dragging = true;
    if(!this.delta)
      this.delta = this.currentDelta();

    if(this.options.zindex) {
      this.originalZ = parseInt(Element.getStyle(this.element,'z-index') || 0);
      this.element.style.zIndex = this.options.zindex;
    }

    if(this.options.ghosting) {
      this._clone = this.element.cloneNode(true);
      this._originallyAbsolute = (this.element.getStyle('position') == 'absolute');
      if (!this._originallyAbsolute)
        Position.absolutize(this.element);
      this.element.parentNode.insertBefore(this._clone, this.element);
    }

    if(this.options.scroll) {
      if (this.options.scroll == window) {
        var where = this._getWindowScroll(this.options.scroll);
        this.originalScrollLeft = where.left;
        this.originalScrollTop = where.top;
      } else {
        this.originalScrollLeft = this.options.scroll.scrollLeft;
        this.originalScrollTop = this.options.scroll.scrollTop;
      }
    }

    Draggables.notify('onStart', this, event);

    if(this.options.starteffect) this.options.starteffect(this.element);
  },

  updateDrag: function(event, pointer) {
    if(!this.dragging) this.startDrag(event);

    if(!this.options.quiet){
      Position.prepare();
      Droppables.show(pointer, this.element);
    }

    Draggables.notify('onDrag', this, event);

    this.draw(pointer);
    if(this.options.change) this.options.change(this);

    if(this.options.scroll) {
      this.stopScrolling();

      var p;
      if (this.options.scroll == window) {
        with(this._getWindowScroll(this.options.scroll)) { p = [ left, top, left+width, top+height ]; }
      } else {
        p = Position.page(this.options.scroll);
        p[0] += this.options.scroll.scrollLeft + Position.deltaX;
        p[1] += this.options.scroll.scrollTop + Position.deltaY;
        p.push(p[0]+this.options.scroll.offsetWidth);
        p.push(p[1]+this.options.scroll.offsetHeight);
      }
      var speed = [0,0];
      if(pointer[0] < (p[0]+this.options.scrollSensitivity)) speed[0] = pointer[0]-(p[0]+this.options.scrollSensitivity);
      if(pointer[1] < (p[1]+this.options.scrollSensitivity)) speed[1] = pointer[1]-(p[1]+this.options.scrollSensitivity);
      if(pointer[0] > (p[2]-this.options.scrollSensitivity)) speed[0] = pointer[0]-(p[2]-this.options.scrollSensitivity);
      if(pointer[1] > (p[3]-this.options.scrollSensitivity)) speed[1] = pointer[1]-(p[3]-this.options.scrollSensitivity);
      this.startScrolling(speed);
    }

    // fix AppleWebKit rendering
    if(Prototype.Browser.WebKit) window.scrollBy(0,0);

    Event.stop(event);
  },

  finishDrag: function(event, success) {
    this.dragging = false;

    if(this.options.quiet){
      Position.prepare();
      var pointer = [Event.pointerX(event), Event.pointerY(event)];
      Droppables.show(pointer, this.element);
    }

    if(this.options.ghosting) {
      if (!this._originallyAbsolute)
        Position.relativize(this.element);
      delete this._originallyAbsolute;
      Element.remove(this._clone);
      this._clone = null;
    }

    var dropped = false;
    if(success) {
      dropped = Droppables.fire(event, this.element);
      if (!dropped) dropped = false;
    }
    if(dropped && this.options.onDropped) this.options.onDropped(this.element);
    Draggables.notify('onEnd', this, event);

    var revert = this.options.revert;
    if(revert && Object.isFunction(revert)) revert = revert(this.element);

    var d = this.currentDelta();
    if(revert && this.options.reverteffect) {
      if (dropped == 0 || revert != 'failure')
        this.options.reverteffect(this.element,
          d[1]-this.delta[1], d[0]-this.delta[0]);
    } else {
      this.delta = d;
    }

    if(this.options.zindex)
      this.element.style.zIndex = this.originalZ;

    if(this.options.endeffect)
      this.options.endeffect(this.element);

    Draggables.deactivate(this);
    Droppables.reset();
  },

  keyPress: function(event) {
    if(event.keyCode!=Event.KEY_ESC) return;
    this.finishDrag(event, false);
    Event.stop(event);
  },

  endDrag: function(event) {
    if(!this.dragging) return;
    this.stopScrolling();
    this.finishDrag(event, true);
    Event.stop(event);
  },

  draw: function(point) {
    var pos = Position.cumulativeOffset(this.element);
    if(this.options.ghosting) {
      var r   = Position.realOffset(this.element);
      pos[0] += r[0] - Position.deltaX; pos[1] += r[1] - Position.deltaY;
    }

    var d = this.currentDelta();
    pos[0] -= d[0]; pos[1] -= d[1];

    if(this.options.scroll && (this.options.scroll != window && this._isScrollChild)) {
      pos[0] -= this.options.scroll.scrollLeft-this.originalScrollLeft;
      pos[1] -= this.options.scroll.scrollTop-this.originalScrollTop;
    }

    var p = [0,1].map(function(i){
      return (point[i]-pos[i]-this.offset[i])
    }.bind(this));

    if(this.options.snap) {
      if(Object.isFunction(this.options.snap)) {
        p = this.options.snap(p[0],p[1],this);
      } else {
      if(Object.isArray(this.options.snap)) {
        p = p.map( function(v, i) {
          return (v/this.options.snap[i]).round()*this.options.snap[i] }.bind(this));
      } else {
        p = p.map( function(v) {
          return (v/this.options.snap).round()*this.options.snap }.bind(this));
      }
    }}

    var style = this.element.style;
    if((!this.options.constraint) || (this.options.constraint=='horizontal'))
      style.left = p[0] + "px";
    if((!this.options.constraint) || (this.options.constraint=='vertical'))
      style.top  = p[1] + "px";

    if(style.visibility=="hidden") style.visibility = ""; // fix gecko rendering
  },

  stopScrolling: function() {
    if(this.scrollInterval) {
      clearInterval(this.scrollInterval);
      this.scrollInterval = null;
      Draggables._lastScrollPointer = null;
    }
  },

  startScrolling: function(speed) {
    if(!(speed[0] || speed[1])) return;
    this.scrollSpeed = [speed[0]*this.options.scrollSpeed,speed[1]*this.options.scrollSpeed];
    this.lastScrolled = new Date();
    this.scrollInterval = setInterval(this.scroll.bind(this), 10);
  },

  scroll: function() {
    var current = new Date();
    var delta = current - this.lastScrolled;
    this.lastScrolled = current;
    if(this.options.scroll == window) {
      with (this._getWindowScroll(this.options.scroll)) {
        if (this.scrollSpeed[0] || this.scrollSpeed[1]) {
          var d = delta / 1000;
          this.options.scroll.scrollTo( left + d*this.scrollSpeed[0], top + d*this.scrollSpeed[1] );
        }
      }
    } else {
      this.options.scroll.scrollLeft += this.scrollSpeed[0] * delta / 1000;
      this.options.scroll.scrollTop  += this.scrollSpeed[1] * delta / 1000;
    }

    Position.prepare();
    Droppables.show(Draggables._lastPointer, this.element);
    Draggables.notify('onDrag', this);
    if (this._isScrollChild) {
      Draggables._lastScrollPointer = Draggables._lastScrollPointer || $A(Draggables._lastPointer);
      Draggables._lastScrollPointer[0] += this.scrollSpeed[0] * delta / 1000;
      Draggables._lastScrollPointer[1] += this.scrollSpeed[1] * delta / 1000;
      if (Draggables._lastScrollPointer[0] < 0)
        Draggables._lastScrollPointer[0] = 0;
      if (Draggables._lastScrollPointer[1] < 0)
        Draggables._lastScrollPointer[1] = 0;
      this.draw(Draggables._lastScrollPointer);
    }

    if(this.options.change) this.options.change(this);
  },

  _getWindowScroll: function(w) {
    var T, L, W, H;
    with (w.document) {
      if (w.document.documentElement && documentElement.scrollTop) {
        T = documentElement.scrollTop;
        L = documentElement.scrollLeft;
      } else if (w.document.body) {
        T = body.scrollTop;
        L = body.scrollLeft;
      }
      if (w.innerWidth) {
        W = w.innerWidth;
        H = w.innerHeight;
      } else if (w.document.documentElement && documentElement.clientWidth) {
        W = documentElement.clientWidth;
        H = documentElement.clientHeight;
      } else {
        W = body.offsetWidth;
        H = body.offsetHeight;
      }
    }
    return { top: T, left: L, width: W, height: H };
  }
});

Draggable._dragging = { };

/*--------------------------------------------------------------------------*/

var SortableObserver = Class.create({
  initialize: function(element, observer) {
    this.element   = $(element);
    this.observer  = observer;
    this.lastValue = Sortable.serialize(this.element);
  },

  onStart: function() {
    this.lastValue = Sortable.serialize(this.element);
  },

  onEnd: function() {
    Sortable.unmark();
    if(this.lastValue != Sortable.serialize(this.element))
      this.observer(this.element)
  }
});

var Sortable = {
  SERIALIZE_RULE: /^[^_\-](?:[A-Za-z0-9\-\_]*)[_](.*)$/,

  sortables: { },

  _findRootElement: function(element) {
    while (element.tagName.toUpperCase() != "BODY") {
      if(element.id && Sortable.sortables[element.id]) return element;
      element = element.parentNode;
    }
  },

  options: function(element) {
    element = Sortable._findRootElement($(element));
    if(!element) return;
    return Sortable.sortables[element.id];
  },

  destroy: function(element){
    element = $(element);
    var s = Sortable.sortables[element.id];

    if(s) {
      Draggables.removeObserver(s.element);
      s.droppables.each(function(d){ Droppables.remove(d) });
      s.draggables.invoke('destroy');

      delete Sortable.sortables[s.element.id];
    }
  },

  create: function(element) {
    element = $(element);
    var options = Object.extend({
      element:     element,
      tag:         'li',       // assumes li children, override with tag: 'tagname'
      dropOnEmpty: false,
      tree:        false,
      treeTag:     'ul',
      overlap:     'vertical', // one of 'vertical', 'horizontal'
      constraint:  'vertical', // one of 'vertical', 'horizontal', false
      containment: element,    // also takes array of elements (or id's); or false
      handle:      false,      // or a CSS class
      only:        false,
      delay:       0,
      hoverclass:  null,
      ghosting:    false,
      quiet:       false,
      scroll:      false,
      scrollSensitivity: 20,
      scrollSpeed: 15,
      format:      this.SERIALIZE_RULE,

      // these take arrays of elements or ids and can be
      // used for better initialization performance
      elements:    false,
      handles:     false,

      onChange:    Prototype.emptyFunction,
      onUpdate:    Prototype.emptyFunction
    }, arguments[1] || { });

    // clear any old sortable with same element
    this.destroy(element);

    // build options for the draggables
    var options_for_draggable = {
      revert:      true,
      quiet:       options.quiet,
      scroll:      options.scroll,
      scrollSpeed: options.scrollSpeed,
      scrollSensitivity: options.scrollSensitivity,
      delay:       options.delay,
      ghosting:    options.ghosting,
      constraint:  options.constraint,
      handle:      options.handle };

    if(options.starteffect)
      options_for_draggable.starteffect = options.starteffect;

    if(options.reverteffect)
      options_for_draggable.reverteffect = options.reverteffect;
    else
      if(options.ghosting) options_for_draggable.reverteffect = function(element) {
        element.style.top  = 0;
        element.style.left = 0;
      };

    if(options.endeffect)
      options_for_draggable.endeffect = options.endeffect;

    if(options.zindex)
      options_for_draggable.zindex = options.zindex;

    // build options for the droppables
    var options_for_droppable = {
      overlap:     options.overlap,
      containment: options.containment,
      tree:        options.tree,
      hoverclass:  options.hoverclass,
      onHover:     Sortable.onHover
    };

    var options_for_tree = {
      onHover:      Sortable.onEmptyHover,
      overlap:      options.overlap,
      containment:  options.containment,
      hoverclass:   options.hoverclass
    };

    // fix for gecko engine
    Element.cleanWhitespace(element);

    options.draggables = [];
    options.droppables = [];

    // drop on empty handling
    if(options.dropOnEmpty || options.tree) {
      Droppables.add(element, options_for_tree);
      options.droppables.push(element);
    }

    (options.elements || this.findElements(element, options) || []).each( function(e,i) {
      var handle = options.handles ? $(options.handles[i]) :
        (options.handle ? $(e).select('.' + options.handle)[0] : e);
      options.draggables.push(
        new Draggable(e, Object.extend(options_for_draggable, { handle: handle })));
      Droppables.add(e, options_for_droppable);
      if(options.tree) e.treeNode = element;
      options.droppables.push(e);
    });

    if(options.tree) {
      (Sortable.findTreeElements(element, options) || []).each( function(e) {
        Droppables.add(e, options_for_tree);
        e.treeNode = element;
        options.droppables.push(e);
      });
    }

    // keep reference
    this.sortables[element.id] = options;

    // for onupdate
    Draggables.addObserver(new SortableObserver(element, options.onUpdate));

  },

  // return all suitable-for-sortable elements in a guaranteed order
  findElements: function(element, options) {
    return Element.findChildren(
      element, options.only, options.tree ? true : false, options.tag);
  },

  findTreeElements: function(element, options) {
    return Element.findChildren(
      element, options.only, options.tree ? true : false, options.treeTag);
  },

  onHover: function(element, dropon, overlap) {
    if(Element.isParent(dropon, element)) return;

    if(overlap > .33 && overlap < .66 && Sortable.options(dropon).tree) {
      return;
    } else if(overlap>0.5) {
      Sortable.mark(dropon, 'before');
      if(dropon.previousSibling != element) {
        var oldParentNode = element.parentNode;
        element.style.visibility = "hidden"; // fix gecko rendering
        dropon.parentNode.insertBefore(element, dropon);
        if(dropon.parentNode!=oldParentNode)
          Sortable.options(oldParentNode).onChange(element);
        Sortable.options(dropon.parentNode).onChange(element);
      }
    } else {
      Sortable.mark(dropon, 'after');
      var nextElement = dropon.nextSibling || null;
      if(nextElement != element) {
        var oldParentNode = element.parentNode;
        element.style.visibility = "hidden"; // fix gecko rendering
        dropon.parentNode.insertBefore(element, nextElement);
        if(dropon.parentNode!=oldParentNode)
          Sortable.options(oldParentNode).onChange(element);
        Sortable.options(dropon.parentNode).onChange(element);
      }
    }
  },

  onEmptyHover: function(element, dropon, overlap) {
    var oldParentNode = element.parentNode;
    var droponOptions = Sortable.options(dropon);

    if(!Element.isParent(dropon, element)) {
      var index;

      var children = Sortable.findElements(dropon, {tag: droponOptions.tag, only: droponOptions.only});
      var child = null;

      if(children) {
        var offset = Element.offsetSize(dropon, droponOptions.overlap) * (1.0 - overlap);

        for (index = 0; index < children.length; index += 1) {
          if (offset - Element.offsetSize (children[index], droponOptions.overlap) >= 0) {
            offset -= Element.offsetSize (children[index], droponOptions.overlap);
          } else if (offset - (Element.offsetSize (children[index], droponOptions.overlap) / 2) >= 0) {
            child = index + 1 < children.length ? children[index + 1] : null;
            break;
          } else {
            child = children[index];
            break;
          }
        }
      }

      dropon.insertBefore(element, child);

      Sortable.options(oldParentNode).onChange(element);
      droponOptions.onChange(element);
    }
  },

  unmark: function() {
    if(Sortable._marker) Sortable._marker.hide();
  },

  mark: function(dropon, position) {
    // mark on ghosting only
    var sortable = Sortable.options(dropon.parentNode);
    if(sortable && !sortable.ghosting) return;

    if(!Sortable._marker) {
      Sortable._marker =
        ($('dropmarker') || Element.extend(document.createElement('DIV'))).
          hide().addClassName('dropmarker').setStyle({position:'absolute'});
      document.getElementsByTagName("body").item(0).appendChild(Sortable._marker);
    }
    var offsets = Position.cumulativeOffset(dropon);
    Sortable._marker.setStyle({left: offsets[0]+'px', top: offsets[1] + 'px'});

    if(position=='after')
      if(sortable.overlap == 'horizontal')
        Sortable._marker.setStyle({left: (offsets[0]+dropon.clientWidth) + 'px'});
      else
        Sortable._marker.setStyle({top: (offsets[1]+dropon.clientHeight) + 'px'});

    Sortable._marker.show();
  },

  _tree: function(element, options, parent) {
    var children = Sortable.findElements(element, options) || [];

    for (var i = 0; i < children.length; ++i) {
      var match = children[i].id.match(options.format);

      if (!match) continue;

      var child = {
        id: encodeURIComponent(match ? match[1] : null),
        element: element,
        parent: parent,
        children: [],
        position: parent.children.length,
        container: $(children[i]).down(options.treeTag)
      };

      /* Get the element containing the children and recurse over it */
      if (child.container)
        this._tree(child.container, options, child);

      parent.children.push (child);
    }

    return parent;
  },

  tree: function(element) {
    element = $(element);
    var sortableOptions = this.options(element);
    var options = Object.extend({
      tag: sortableOptions.tag,
      treeTag: sortableOptions.treeTag,
      only: sortableOptions.only,
      name: element.id,
      format: sortableOptions.format
    }, arguments[1] || { });

    var root = {
      id: null,
      parent: null,
      children: [],
      container: element,
      position: 0
    };

    return Sortable._tree(element, options, root);
  },

  /* Construct a [i] index for a particular node */
  _constructIndex: function(node) {
    var index = '';
    do {
      if (node.id) index = '[' + node.position + ']' + index;
    } while ((node = node.parent) != null);
    return index;
  },

  sequence: function(element) {
    element = $(element);
    var options = Object.extend(this.options(element), arguments[1] || { });

    return $(this.findElements(element, options) || []).map( function(item) {
      return item.id.match(options.format) ? item.id.match(options.format)[1] : '';
    });
  },

  setSequence: function(element, new_sequence) {
    element = $(element);
    var options = Object.extend(this.options(element), arguments[2] || { });

    var nodeMap = { };
    this.findElements(element, options).each( function(n) {
        if (n.id.match(options.format))
            nodeMap[n.id.match(options.format)[1]] = [n, n.parentNode];
        n.parentNode.removeChild(n);
    });

    new_sequence.each(function(ident) {
      var n = nodeMap[ident];
      if (n) {
        n[1].appendChild(n[0]);
        delete nodeMap[ident];
      }
    });
  },

  serialize: function(element) {
    element = $(element);
    var options = Object.extend(Sortable.options(element), arguments[1] || { });
    var name = encodeURIComponent(
      (arguments[1] && arguments[1].name) ? arguments[1].name : element.id);

    if (options.tree) {
      return Sortable.tree(element, arguments[1]).children.map( function (item) {
        return [name + Sortable._constructIndex(item) + "[id]=" +
                encodeURIComponent(item.id)].concat(item.children.map(arguments.callee));
      }).flatten().join('&');
    } else {
      return Sortable.sequence(element, arguments[1]).map( function(item) {
        return name + "[]=" + encodeURIComponent(item);
      }).join('&');
    }
  }
};

// Returns true if child is contained within element
Element.isParent = function(child, element) {
  if (!child.parentNode || child == element) return false;
  if (child.parentNode == element) return true;
  return Element.isParent(child.parentNode, element);
};

Element.findChildren = function(element, only, recursive, tagName) {
  if(!element.hasChildNodes()) return null;
  tagName = tagName.toUpperCase();
  if(only) only = [only].flatten();
  var elements = [];
  $A(element.childNodes).each( function(e) {
    if(e.tagName && e.tagName.toUpperCase()==tagName &&
      (!only || (Element.classNames(e).detect(function(v) { return only.include(v) }))))
        elements.push(e);
    if(recursive) {
      var grandchildren = Element.findChildren(e, only, recursive, tagName);
      if(grandchildren) elements.push(grandchildren);
    }
  });

  return (elements.length>0 ? elements.flatten() : []);
};

Element.offsetSize = function (element, type) {
  return element['offset' + ((type=='vertical' || type=='height') ? 'Height' : 'Width')];
};// script.aculo.us controls.js v1.8.2, Tue Nov 18 18:30:58 +0100 2008

// Copyright (c) 2005-2008 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us)
//           (c) 2005-2008 Ivan Krstic (http://blogs.law.harvard.edu/ivan)
//           (c) 2005-2008 Jon Tirsen (http://www.tirsen.com)
// Contributors:
//  Richard Livsey
//  Rahul Bhargava
//  Rob Wills
//
// script.aculo.us is freely distributable under the terms of an MIT-style license.
// For details, see the script.aculo.us web site: http://script.aculo.us/

// Autocompleter.Base handles all the autocompletion functionality
// that's independent of the data source for autocompletion. This
// includes drawing the autocompletion menu, observing keyboard
// and mouse events, and similar.
//
// Specific autocompleters need to provide, at the very least,
// a getUpdatedChoices function that will be invoked every time
// the text inside the monitored textbox changes. This method
// should get the text for which to provide autocompletion by
// invoking this.getToken(), NOT by directly accessing
// this.element.value. This is to allow incremental tokenized
// autocompletion. Specific auto-completion logic (AJAX, etc)
// belongs in getUpdatedChoices.
//
// Tokenized incremental autocompletion is enabled automatically
// when an autocompleter is instantiated with the 'tokens' option
// in the options parameter, e.g.:
// new Ajax.Autocompleter('id','upd', '/url/', { tokens: ',' });
// will incrementally autocomplete with a comma as the token.
// Additionally, ',' in the above example can be replaced with
// a token array, e.g. { tokens: [',', '\n'] } which
// enables autocompletion on multiple tokens. This is most
// useful when one of the tokens is \n (a newline), as it
// allows smart autocompletion after linebreaks.

if(typeof Effect == 'undefined')
  throw("controls.js requires including script.aculo.us' effects.js library");

var Autocompleter = { };
Autocompleter.Base = Class.create({
  baseInitialize: function(element, update, options) {
    element          = $(element);
    this.element     = element;
    this.update      = $(update);
    this.hasFocus    = false;
    this.changed     = false;
    this.active      = false;
    this.index       = 0;
    this.entryCount  = 0;
    this.oldElementValue = this.element.value;

    if(this.setOptions)
      this.setOptions(options);
    else
      this.options = options || { };

    this.options.paramName    = this.options.paramName || this.element.name;
    this.options.tokens       = this.options.tokens || [];
    this.options.frequency    = this.options.frequency || 0.4;
    this.options.minChars     = this.options.minChars || 1;
    this.options.onShow       = this.options.onShow ||
      function(element, update){
        if(!update.style.position || update.style.position=='absolute') {
          update.style.position = 'absolute';
          Position.clone(element, update, {
            setHeight: false,
            offsetTop: element.offsetHeight
          });
        }
        Effect.Appear(update,{duration:0.15});
      };
    this.options.onHide = this.options.onHide ||
      function(element, update){ new Effect.Fade(update,{duration:0.15}) };

    if(typeof(this.options.tokens) == 'string')
      this.options.tokens = new Array(this.options.tokens);
    // Force carriage returns as token delimiters anyway
    if (!this.options.tokens.include('\n'))
      this.options.tokens.push('\n');

    this.observer = null;

    this.element.setAttribute('autocomplete','off');

    Element.hide(this.update);

    Event.observe(this.element, 'blur', this.onBlur.bindAsEventListener(this));
    Event.observe(this.element, 'keydown', this.onKeyPress.bindAsEventListener(this));
  },

  show: function() {
    if(Element.getStyle(this.update, 'display')=='none') this.options.onShow(this.element, this.update);
    if(!this.iefix &&
      (Prototype.Browser.IE) &&
      (Element.getStyle(this.update, 'position')=='absolute')) {
      new Insertion.After(this.update,
       '<iframe id="' + this.update.id + '_iefix" '+
       'style="display:none;position:absolute;filter:progid:DXImageTransform.Microsoft.Alpha(opacity=0);" ' +
       'src="javascript:false;" frameborder="0" scrolling="no"></iframe>');
      this.iefix = $(this.update.id+'_iefix');
    }
    if(this.iefix) setTimeout(this.fixIEOverlapping.bind(this), 50);
  },

  fixIEOverlapping: function() {
    Position.clone(this.update, this.iefix, {setTop:(!this.update.style.height)});
    this.iefix.style.zIndex = 1;
    this.update.style.zIndex = 2;
    Element.show(this.iefix);
  },

  hide: function() {
    this.stopIndicator();
    if(Element.getStyle(this.update, 'display')!='none') this.options.onHide(this.element, this.update);
    if(this.iefix) Element.hide(this.iefix);
  },

  startIndicator: function() {
    if(this.options.indicator) Element.show(this.options.indicator);
  },

  stopIndicator: function() {
    if(this.options.indicator) Element.hide(this.options.indicator);
  },

  onKeyPress: function(event) {
    if(this.active)
      switch(event.keyCode) {
       case Event.KEY_TAB:
       case Event.KEY_RETURN:
         this.selectEntry();
         Event.stop(event);
       case Event.KEY_ESC:
         this.hide();
         this.active = false;
         Event.stop(event);
         return;
       case Event.KEY_LEFT:
       case Event.KEY_RIGHT:
         return;
       case Event.KEY_UP:
         this.markPrevious();
         this.render();
         Event.stop(event);
         return;
       case Event.KEY_DOWN:
         this.markNext();
         this.render();
         Event.stop(event);
         return;
      }
     else
       if(event.keyCode==Event.KEY_TAB || event.keyCode==Event.KEY_RETURN ||
         (Prototype.Browser.WebKit > 0 && event.keyCode == 0)) return;

    this.changed = true;
    this.hasFocus = true;

    if(this.observer) clearTimeout(this.observer);
      this.observer =
        setTimeout(this.onObserverEvent.bind(this), this.options.frequency*1000);
  },

  activate: function() {
    this.changed = false;
    this.hasFocus = true;
    this.getUpdatedChoices();
  },

  onHover: function(event) {
    var element = Event.findElement(event, 'LI');
    if(this.index != element.autocompleteIndex)
    {
        this.index = element.autocompleteIndex;
        this.render();
    }
    Event.stop(event);
  },

  onClick: function(event) {
    var element = Event.findElement(event, 'LI');
    this.index = element.autocompleteIndex;
    this.selectEntry();
    this.hide();
  },

  onBlur: function(event) {
    // needed to make click events working
    setTimeout(this.hide.bind(this), 250);
    this.hasFocus = false;
    this.active = false;
  },

  render: function() {
    if(this.entryCount > 0) {
      for (var i = 0; i < this.entryCount; i++)
        this.index==i ?
          Element.addClassName(this.getEntry(i),"selected") :
          Element.removeClassName(this.getEntry(i),"selected");
      if(this.hasFocus) {
        this.show();
        this.active = true;
      }
    } else {
      this.active = false;
      this.hide();
    }
  },

  markPrevious: function() {
    if(this.index > 0) this.index--;
      else this.index = this.entryCount-1;
    this.getEntry(this.index).scrollIntoView(true);
  },

  markNext: function() {
    if(this.index < this.entryCount-1) this.index++;
      else this.index = 0;
    this.getEntry(this.index).scrollIntoView(false);
  },

  getEntry: function(index) {
    return this.update.firstChild.childNodes[index];
  },

  getCurrentEntry: function() {
    return this.getEntry(this.index);
  },

  selectEntry: function() {
    this.active = false;
    this.updateElement(this.getCurrentEntry());
  },

  updateElement: function(selectedElement) {
    if (this.options.updateElement) {
      this.options.updateElement(selectedElement);
      return;
    }
    var value = '';
    if (this.options.select) {
      var nodes = $(selectedElement).select('.' + this.options.select) || [];
      if(nodes.length>0) value = Element.collectTextNodes(nodes[0], this.options.select);
    } else
      value = Element.collectTextNodesIgnoreClass(selectedElement, 'informal');

    var bounds = this.getTokenBounds();
    if (bounds[0] != -1) {
      var newValue = this.element.value.substr(0, bounds[0]);
      var whitespace = this.element.value.substr(bounds[0]).match(/^\s+/);
      if (whitespace)
        newValue += whitespace[0];
      this.element.value = newValue + value + this.element.value.substr(bounds[1]);
    } else {
      this.element.value = value;
    }
    this.oldElementValue = this.element.value;
    this.element.focus();

    if (this.options.afterUpdateElement)
      this.options.afterUpdateElement(this.element, selectedElement);
  },

  updateChoices: function(choices) {
    if(!this.changed && this.hasFocus) {
      this.update.innerHTML = choices;
      Element.cleanWhitespace(this.update);
      Element.cleanWhitespace(this.update.down());

      if(this.update.firstChild && this.update.down().childNodes) {
        this.entryCount =
          this.update.down().childNodes.length;
        for (var i = 0; i < this.entryCount; i++) {
          var entry = this.getEntry(i);
          entry.autocompleteIndex = i;
          this.addObservers(entry);
        }
      } else {
        this.entryCount = 0;
      }

      this.stopIndicator();
      this.index = 0;

      if(this.entryCount==1 && this.options.autoSelect) {
        this.selectEntry();
        this.hide();
      } else {
        this.render();
      }
    }
  },

  addObservers: function(element) {
    Event.observe(element, "mouseover", this.onHover.bindAsEventListener(this));
    Event.observe(element, "click", this.onClick.bindAsEventListener(this));
  },

  onObserverEvent: function() {
    this.changed = false;
    this.tokenBounds = null;
    if(this.getToken().length>=this.options.minChars) {
      this.getUpdatedChoices();
    } else {
      this.active = false;
      this.hide();
    }
    this.oldElementValue = this.element.value;
  },

  getToken: function() {
    var bounds = this.getTokenBounds();
    return this.element.value.substring(bounds[0], bounds[1]).strip();
  },

  getTokenBounds: function() {
    if (null != this.tokenBounds) return this.tokenBounds;
    var value = this.element.value;
    if (value.strip().empty()) return [-1, 0];
    var diff = arguments.callee.getFirstDifferencePos(value, this.oldElementValue);
    var offset = (diff == this.oldElementValue.length ? 1 : 0);
    var prevTokenPos = -1, nextTokenPos = value.length;
    var tp;
    for (var index = 0, l = this.options.tokens.length; index < l; ++index) {
      tp = value.lastIndexOf(this.options.tokens[index], diff + offset - 1);
      if (tp > prevTokenPos) prevTokenPos = tp;
      tp = value.indexOf(this.options.tokens[index], diff + offset);
      if (-1 != tp && tp < nextTokenPos) nextTokenPos = tp;
    }
    return (this.tokenBounds = [prevTokenPos + 1, nextTokenPos]);
  }
});

Autocompleter.Base.prototype.getTokenBounds.getFirstDifferencePos = function(newS, oldS) {
  var boundary = Math.min(newS.length, oldS.length);
  for (var index = 0; index < boundary; ++index)
    if (newS[index] != oldS[index])
      return index;
  return boundary;
};

Ajax.Autocompleter = Class.create(Autocompleter.Base, {
  initialize: function(element, update, url, options) {
    this.baseInitialize(element, update, options);
    this.options.asynchronous  = true;
    this.options.onComplete    = this.onComplete.bind(this);
    this.options.defaultParams = this.options.parameters || null;
    this.url                   = url;
  },

  getUpdatedChoices: function() {
    this.startIndicator();

    var entry = encodeURIComponent(this.options.paramName) + '=' +
      encodeURIComponent(this.getToken());

    this.options.parameters = this.options.callback ?
      this.options.callback(this.element, entry) : entry;

    if(this.options.defaultParams)
      this.options.parameters += '&' + this.options.defaultParams;

    new Ajax.Request(this.url, this.options);
  },

  onComplete: function(request) {
    this.updateChoices(request.responseText);
  }
});

// The local array autocompleter. Used when you'd prefer to
// inject an array of autocompletion options into the page, rather
// than sending out Ajax queries, which can be quite slow sometimes.
//
// The constructor takes four parameters. The first two are, as usual,
// the id of the monitored textbox, and id of the autocompletion menu.
// The third is the array you want to autocomplete from, and the fourth
// is the options block.
//
// Extra local autocompletion options:
// - choices - How many autocompletion choices to offer
//
// - partialSearch - If false, the autocompleter will match entered
//                    text only at the beginning of strings in the
//                    autocomplete array. Defaults to true, which will
//                    match text at the beginning of any *word* in the
//                    strings in the autocomplete array. If you want to
//                    search anywhere in the string, additionally set
//                    the option fullSearch to true (default: off).
//
// - fullSsearch - Search anywhere in autocomplete array strings.
//
// - partialChars - How many characters to enter before triggering
//                   a partial match (unlike minChars, which defines
//                   how many characters are required to do any match
//                   at all). Defaults to 2.
//
// - ignoreCase - Whether to ignore case when autocompleting.
//                 Defaults to true.
//
// It's possible to pass in a custom function as the 'selector'
// option, if you prefer to write your own autocompletion logic.
// In that case, the other options above will not apply unless
// you support them.

Autocompleter.Local = Class.create(Autocompleter.Base, {
  initialize: function(element, update, array, options) {
    this.baseInitialize(element, update, options);
    this.options.array = array;
  },

  getUpdatedChoices: function() {
    this.updateChoices(this.options.selector(this));
  },

  setOptions: function(options) {
    this.options = Object.extend({
      choices: 10,
      partialSearch: true,
      partialChars: 2,
      ignoreCase: true,
      fullSearch: false,
      selector: function(instance) {
        var ret       = []; // Beginning matches
        var partial   = []; // Inside matches
        var entry     = instance.getToken();
        var count     = 0;

        for (var i = 0; i < instance.options.array.length &&
          ret.length < instance.options.choices ; i++) {

          var elem = instance.options.array[i];
          var foundPos = instance.options.ignoreCase ?
            elem.toLowerCase().indexOf(entry.toLowerCase()) :
            elem.indexOf(entry);

          while (foundPos != -1) {
            if (foundPos == 0 && elem.length != entry.length) {
              ret.push("<li><strong>" + elem.substr(0, entry.length) + "</strong>" +
                elem.substr(entry.length) + "</li>");
              break;
            } else if (entry.length >= instance.options.partialChars &&
              instance.options.partialSearch && foundPos != -1) {
              if (instance.options.fullSearch || /\s/.test(elem.substr(foundPos-1,1))) {
                partial.push("<li>" + elem.substr(0, foundPos) + "<strong>" +
                  elem.substr(foundPos, entry.length) + "</strong>" + elem.substr(
                  foundPos + entry.length) + "</li>");
                break;
              }
            }

            foundPos = instance.options.ignoreCase ?
              elem.toLowerCase().indexOf(entry.toLowerCase(), foundPos + 1) :
              elem.indexOf(entry, foundPos + 1);

          }
        }
        if (partial.length)
          ret = ret.concat(partial.slice(0, instance.options.choices - ret.length));
        return "<ul>" + ret.join('') + "</ul>";
      }
    }, options || { });
  }
});

// AJAX in-place editor and collection editor
// Full rewrite by Christophe Porteneuve <tdd@tddsworld.com> (April 2007).

// Use this if you notice weird scrolling problems on some browsers,
// the DOM might be a bit confused when this gets called so do this
// waits 1 ms (with setTimeout) until it does the activation
Field.scrollFreeActivate = function(field) {
  setTimeout(function() {
    Field.activate(field);
  }, 1);
};

Ajax.InPlaceEditor = Class.create({
  initialize: function(element, url, options) {
    this.url = url;
    this.element = element = $(element);
    this.prepareOptions();
    this._controls = { };
    arguments.callee.dealWithDeprecatedOptions(options); // DEPRECATION LAYER!!!
    Object.extend(this.options, options || { });
    if (!this.options.formId && this.element.id) {
      this.options.formId = this.element.id + '-inplaceeditor';
      if ($(this.options.formId))
        this.options.formId = '';
    }
    if (this.options.externalControl)
      this.options.externalControl = $(this.options.externalControl);
    if (!this.options.externalControl)
      this.options.externalControlOnly = false;
    this._originalBackground = this.element.getStyle('background-color') || 'transparent';
    this.element.title = this.options.clickToEditText;
    this._boundCancelHandler = this.handleFormCancellation.bind(this);
    this._boundComplete = (this.options.onComplete || Prototype.emptyFunction).bind(this);
    this._boundFailureHandler = this.handleAJAXFailure.bind(this);
    this._boundSubmitHandler = this.handleFormSubmission.bind(this);
    this._boundWrapperHandler = this.wrapUp.bind(this);
    this.registerListeners();
  },
  checkForEscapeOrReturn: function(e) {
    if (!this._editing || e.ctrlKey || e.altKey || e.shiftKey) return;
    if (Event.KEY_ESC == e.keyCode)
      this.handleFormCancellation(e);
    else if (Event.KEY_RETURN == e.keyCode)
      this.handleFormSubmission(e);
  },
  createControl: function(mode, handler, extraClasses) {
    var control = this.options[mode + 'Control'];
    var text = this.options[mode + 'Text'];
    if ('button' == control) {
      var btn = document.createElement('input');
      btn.type = 'submit';
      btn.value = text;
      btn.className = 'editor_' + mode + '_button';
      if ('cancel' == mode)
        btn.onclick = this._boundCancelHandler;
      this._form.appendChild(btn);
      this._controls[mode] = btn;
    } else if ('link' == control) {
      var link = document.createElement('a');
      link.href = '#';
      link.appendChild(document.createTextNode(text));
      link.onclick = 'cancel' == mode ? this._boundCancelHandler : this._boundSubmitHandler;
      link.className = 'editor_' + mode + '_link';
      if (extraClasses)
        link.className += ' ' + extraClasses;
      this._form.appendChild(link);
      this._controls[mode] = link;
    }
  },
  createEditField: function() {
    var text = (this.options.loadTextURL ? this.options.loadingText : this.getText());
    var fld;
    if (1 >= this.options.rows && !/\r|\n/.test(this.getText())) {
      fld = document.createElement('input');
      fld.type = 'text';
      var size = this.options.size || this.options.cols || 0;
      if (0 < size) fld.size = size;
    } else {
      fld = document.createElement('textarea');
      fld.rows = (1 >= this.options.rows ? this.options.autoRows : this.options.rows);
      fld.cols = this.options.cols || 40;
    }
    fld.name = this.options.paramName;
    fld.value = text; // No HTML breaks conversion anymore
    fld.className = 'editor_field';
    if (this.options.submitOnBlur)
      fld.onblur = this._boundSubmitHandler;
    this._controls.editor = fld;
    if (this.options.loadTextURL)
      this.loadExternalText();
    this._form.appendChild(this._controls.editor);
  },
  createForm: function() {
    var ipe = this;
    function addText(mode, condition) {
      var text = ipe.options['text' + mode + 'Controls'];
      if (!text || condition === false) return;
      ipe._form.appendChild(document.createTextNode(text));
    };
    this._form = $(document.createElement('form'));
    this._form.id = this.options.formId;
    this._form.addClassName(this.options.formClassName);
    this._form.onsubmit = this._boundSubmitHandler;
    this.createEditField();
    if ('textarea' == this._controls.editor.tagName.toLowerCase())
      this._form.appendChild(document.createElement('br'));
    if (this.options.onFormCustomization)
      this.options.onFormCustomization(this, this._form);
    addText('Before', this.options.okControl || this.options.cancelControl);
    this.createControl('ok', this._boundSubmitHandler);
    addText('Between', this.options.okControl && this.options.cancelControl);
    this.createControl('cancel', this._boundCancelHandler, 'editor_cancel');
    addText('After', this.options.okControl || this.options.cancelControl);
  },
  destroy: function() {
    if (this._oldInnerHTML)
      this.element.innerHTML = this._oldInnerHTML;
    this.leaveEditMode();
    this.unregisterListeners();
  },
  enterEditMode: function(e) {
    if (this._saving || this._editing) return;
    this._editing = true;
    this.triggerCallback('onEnterEditMode');
    if (this.options.externalControl)
      this.options.externalControl.hide();
    this.element.hide();
    this.createForm();
    this.element.parentNode.insertBefore(this._form, this.element);
    if (!this.options.loadTextURL)
      this.postProcessEditField();
    if (e) Event.stop(e);
  },
  enterHover: function(e) {
    if (this.options.hoverClassName)
      this.element.addClassName(this.options.hoverClassName);
    if (this._saving) return;
    this.triggerCallback('onEnterHover');
  },
  getText: function() {
    return this.element.innerHTML.unescapeHTML();
  },
  handleAJAXFailure: function(transport) {
    this.triggerCallback('onFailure', transport);
    if (this._oldInnerHTML) {
      this.element.innerHTML = this._oldInnerHTML;
      this._oldInnerHTML = null;
    }
  },
  handleFormCancellation: function(e) {
    this.wrapUp();
    if (e) Event.stop(e);
  },
  handleFormSubmission: function(e) {
    var form = this._form;
    var value = $F(this._controls.editor);
    this.prepareSubmission();
    var params = this.options.callback(form, value) || '';
    if (Object.isString(params))
      params = params.toQueryParams();
    params.editorId = this.element.id;
    if (this.options.htmlResponse) {
      var options = Object.extend({ evalScripts: true }, this.options.ajaxOptions);
      Object.extend(options, {
        parameters: params,
        onComplete: this._boundWrapperHandler,
        onFailure: this._boundFailureHandler
      });
      new Ajax.Updater({ success: this.element }, this.url, options);
    } else {
      var options = Object.extend({ method: 'get' }, this.options.ajaxOptions);
      Object.extend(options, {
        parameters: params,
        onComplete: this._boundWrapperHandler,
        onFailure: this._boundFailureHandler
      });
      new Ajax.Request(this.url, options);
    }
    if (e) Event.stop(e);
  },
  leaveEditMode: function() {
    this.element.removeClassName(this.options.savingClassName);
    this.removeForm();
    this.leaveHover();
    this.element.style.backgroundColor = this._originalBackground;
    this.element.show();
    if (this.options.externalControl)
      this.options.externalControl.show();
    this._saving = false;
    this._editing = false;
    this._oldInnerHTML = null;
    this.triggerCallback('onLeaveEditMode');
  },
  leaveHover: function(e) {
    if (this.options.hoverClassName)
      this.element.removeClassName(this.options.hoverClassName);
    if (this._saving) return;
    this.triggerCallback('onLeaveHover');
  },
  loadExternalText: function() {
    this._form.addClassName(this.options.loadingClassName);
    this._controls.editor.disabled = true;
    var options = Object.extend({ method: 'get' }, this.options.ajaxOptions);
    Object.extend(options, {
      parameters: 'editorId=' + encodeURIComponent(this.element.id),
      onComplete: Prototype.emptyFunction,
      onSuccess: function(transport) {
        this._form.removeClassName(this.options.loadingClassName);
        var text = transport.responseText;
        if (this.options.stripLoadedTextTags)
          text = text.stripTags();
        this._controls.editor.value = text;
        this._controls.editor.disabled = false;
        this.postProcessEditField();
      }.bind(this),
      onFailure: this._boundFailureHandler
    });
    new Ajax.Request(this.options.loadTextURL, options);
  },
  postProcessEditField: function() {
    var fpc = this.options.fieldPostCreation;
    if (fpc)
      $(this._controls.editor)['focus' == fpc ? 'focus' : 'activate']();
  },
  prepareOptions: function() {
    this.options = Object.clone(Ajax.InPlaceEditor.DefaultOptions);
    Object.extend(this.options, Ajax.InPlaceEditor.DefaultCallbacks);
    [this._extraDefaultOptions].flatten().compact().each(function(defs) {
      Object.extend(this.options, defs);
    }.bind(this));
  },
  prepareSubmission: function() {
    this._saving = true;
    this.removeForm();
    this.leaveHover();
    this.showSaving();
  },
  registerListeners: function() {
    this._listeners = { };
    var listener;
    $H(Ajax.InPlaceEditor.Listeners).each(function(pair) {
      listener = this[pair.value].bind(this);
      this._listeners[pair.key] = listener;
      if (!this.options.externalControlOnly)
        this.element.observe(pair.key, listener);
      if (this.options.externalControl)
        this.options.externalControl.observe(pair.key, listener);
    }.bind(this));
  },
  removeForm: function() {
    if (!this._form) return;
    this._form.remove();
    this._form = null;
    this._controls = { };
  },
  showSaving: function() {
    this._oldInnerHTML = this.element.innerHTML;
    this.element.innerHTML = this.options.savingText;
    this.element.addClassName(this.options.savingClassName);
    this.element.style.backgroundColor = this._originalBackground;
    this.element.show();
  },
  triggerCallback: function(cbName, arg) {
    if ('function' == typeof this.options[cbName]) {
      this.options[cbName](this, arg);
    }
  },
  unregisterListeners: function() {
    $H(this._listeners).each(function(pair) {
      if (!this.options.externalControlOnly)
        this.element.stopObserving(pair.key, pair.value);
      if (this.options.externalControl)
        this.options.externalControl.stopObserving(pair.key, pair.value);
    }.bind(this));
  },
  wrapUp: function(transport) {
    this.leaveEditMode();
    // Can't use triggerCallback due to backward compatibility: requires
    // binding + direct element
    this._boundComplete(transport, this.element);
  }
});

Object.extend(Ajax.InPlaceEditor.prototype, {
  dispose: Ajax.InPlaceEditor.prototype.destroy
});

Ajax.InPlaceCollectionEditor = Class.create(Ajax.InPlaceEditor, {
  initialize: function($super, element, url, options) {
    this._extraDefaultOptions = Ajax.InPlaceCollectionEditor.DefaultOptions;
    $super(element, url, options);
  },

  createEditField: function() {
    var list = document.createElement('select');
    list.name = this.options.paramName;
    list.size = 1;
    this._controls.editor = list;
    this._collection = this.options.collection || [];
    if (this.options.loadCollectionURL)
      this.loadCollection();
    else
      this.checkForExternalText();
    this._form.appendChild(this._controls.editor);
  },

  loadCollection: function() {
    this._form.addClassName(this.options.loadingClassName);
    this.showLoadingText(this.options.loadingCollectionText);
    var options = Object.extend({ method: 'get' }, this.options.ajaxOptions);
    Object.extend(options, {
      parameters: 'editorId=' + encodeURIComponent(this.element.id),
      onComplete: Prototype.emptyFunction,
      onSuccess: function(transport) {
        var js = transport.responseText.strip();
        if (!/^\[.*\]$/.test(js)) // TODO: improve sanity check
          throw('Server returned an invalid collection representation.');
        this._collection = eval(js);
        this.checkForExternalText();
      }.bind(this),
      onFailure: this.onFailure
    });
    new Ajax.Request(this.options.loadCollectionURL, options);
  },

  showLoadingText: function(text) {
    this._controls.editor.disabled = true;
    var tempOption = this._controls.editor.firstChild;
    if (!tempOption) {
      tempOption = document.createElement('option');
      tempOption.value = '';
      this._controls.editor.appendChild(tempOption);
      tempOption.selected = true;
    }
    tempOption.update((text || '').stripScripts().stripTags());
  },

  checkForExternalText: function() {
    this._text = this.getText();
    if (this.options.loadTextURL)
      this.loadExternalText();
    else
      this.buildOptionList();
  },

  loadExternalText: function() {
    this.showLoadingText(this.options.loadingText);
    var options = Object.extend({ method: 'get' }, this.options.ajaxOptions);
    Object.extend(options, {
      parameters: 'editorId=' + encodeURIComponent(this.element.id),
      onComplete: Prototype.emptyFunction,
      onSuccess: function(transport) {
        this._text = transport.responseText.strip();
        this.buildOptionList();
      }.bind(this),
      onFailure: this.onFailure
    });
    new Ajax.Request(this.options.loadTextURL, options);
  },

  buildOptionList: function() {
    this._form.removeClassName(this.options.loadingClassName);
    this._collection = this._collection.map(function(entry) {
      return 2 === entry.length ? entry : [entry, entry].flatten();
    });
    var marker = ('value' in this.options) ? this.options.value : this._text;
    var textFound = this._collection.any(function(entry) {
      return entry[0] == marker;
    }.bind(this));
    this._controls.editor.update('');
    var option;
    this._collection.each(function(entry, index) {
      option = document.createElement('option');
      option.value = entry[0];
      option.selected = textFound ? entry[0] == marker : 0 == index;
      option.appendChild(document.createTextNode(entry[1]));
      this._controls.editor.appendChild(option);
    }.bind(this));
    this._controls.editor.disabled = false;
    Field.scrollFreeActivate(this._controls.editor);
  }
});

//**** DEPRECATION LAYER FOR InPlace[Collection]Editor! ****
//**** This only  exists for a while,  in order to  let ****
//**** users adapt to  the new API.  Read up on the new ****
//**** API and convert your code to it ASAP!            ****

Ajax.InPlaceEditor.prototype.initialize.dealWithDeprecatedOptions = function(options) {
  if (!options) return;
  function fallback(name, expr) {
    if (name in options || expr === undefined) return;
    options[name] = expr;
  };
  fallback('cancelControl', (options.cancelLink ? 'link' : (options.cancelButton ? 'button' :
    options.cancelLink == options.cancelButton == false ? false : undefined)));
  fallback('okControl', (options.okLink ? 'link' : (options.okButton ? 'button' :
    options.okLink == options.okButton == false ? false : undefined)));
  fallback('highlightColor', options.highlightcolor);
  fallback('highlightEndColor', options.highlightendcolor);
};

Object.extend(Ajax.InPlaceEditor, {
  DefaultOptions: {
    ajaxOptions: { },
    autoRows: 3,                                // Use when multi-line w/ rows == 1
    cancelControl: 'link',                      // 'link'|'button'|false
    cancelText: 'cancel',
    clickToEditText: 'Click to edit',
    externalControl: null,                      // id|elt
    externalControlOnly: false,
    fieldPostCreation: 'activate',              // 'activate'|'focus'|false
    formClassName: 'inplaceeditor-form',
    formId: null,                               // id|elt
    highlightColor: '#ffff99',
    highlightEndColor: '#ffffff',
    hoverClassName: '',
    htmlResponse: true,
    loadingClassName: 'inplaceeditor-loading',
    loadingText: 'Loading...',
    okControl: 'button',                        // 'link'|'button'|false
    okText: 'ok',
    paramName: 'value',
    rows: 1,                                    // If 1 and multi-line, uses autoRows
    savingClassName: 'inplaceeditor-saving',
    savingText: 'Saving...',
    size: 0,
    stripLoadedTextTags: false,
    submitOnBlur: false,
    textAfterControls: '',
    textBeforeControls: '',
    textBetweenControls: ''
  },
  DefaultCallbacks: {
    callback: function(form) {
      return Form.serialize(form);
    },
    onComplete: function(transport, element) {
      // For backward compatibility, this one is bound to the IPE, and passes
      // the element directly.  It was too often customized, so we don't break it.
      new Effect.Highlight(element, {
        startcolor: this.options.highlightColor, keepBackgroundImage: true });
    },
    onEnterEditMode: null,
    onEnterHover: function(ipe) {
      ipe.element.style.backgroundColor = ipe.options.highlightColor;
      if (ipe._effect)
        ipe._effect.cancel();
    },
    onFailure: function(transport, ipe) {
      alert('Error communication with the server: ' + transport.responseText.stripTags());
    },
    onFormCustomization: null, // Takes the IPE and its generated form, after editor, before controls.
    onLeaveEditMode: null,
    onLeaveHover: function(ipe) {
      ipe._effect = new Effect.Highlight(ipe.element, {
        startcolor: ipe.options.highlightColor, endcolor: ipe.options.highlightEndColor,
        restorecolor: ipe._originalBackground, keepBackgroundImage: true
      });
    }
  },
  Listeners: {
    click: 'enterEditMode',
    keydown: 'checkForEscapeOrReturn',
    mouseover: 'enterHover',
    mouseout: 'leaveHover'
  }
});

Ajax.InPlaceCollectionEditor.DefaultOptions = {
  loadingCollectionText: 'Loading options...'
};

// Delayed observer, like Form.Element.Observer,
// but waits for delay after last key input
// Ideal for live-search fields

Form.Element.DelayedObserver = Class.create({
  initialize: function(element, delay, callback) {
    this.delay     = delay || 0.5;
    this.element   = $(element);
    this.callback  = callback;
    this.timer     = null;
    this.lastValue = $F(this.element);
    Event.observe(this.element,'keyup',this.delayedListener.bindAsEventListener(this));
  },
  delayedListener: function(event) {
    if(this.lastValue == $F(this.element)) return;
    if(this.timer) clearTimeout(this.timer);
    this.timer = setTimeout(this.onTimerEvent.bind(this), this.delay * 1000);
    this.lastValue = $F(this.element);
  },
  onTimerEvent: function() {
    this.timer = null;
    this.callback(this.element, $F(this.element));
  }
});// script.aculo.us slider.js v1.8.2, Tue Nov 18 18:30:58 +0100 2008

// Copyright (c) 2005-2008 Marty Haught, Thomas Fuchs
//
// script.aculo.us is freely distributable under the terms of an MIT-style license.
// For details, see the script.aculo.us web site: http://script.aculo.us/

if (!Control) var Control = { };

// options:
//  axis: 'vertical', or 'horizontal' (default)
//
// callbacks:
//  onChange(value)
//  onSlide(value)
Control.Slider = Class.create({
  initialize: function(handle, track, options) {
    var slider = this;

    if (Object.isArray(handle)) {
      this.handles = handle.collect( function(e) { return $(e) });
    } else {
      this.handles = [$(handle)];
    }

    this.track   = $(track);
    this.options = options || { };

    this.axis      = this.options.axis || 'horizontal';
    this.increment = this.options.increment || 1;
    this.step      = parseInt(this.options.step || '1');
    this.range     = this.options.range || $R(0,1);

    this.value     = 0; // assure backwards compat
    this.values    = this.handles.map( function() { return 0 });
    this.spans     = this.options.spans ? this.options.spans.map(function(s){ return $(s) }) : false;
    this.options.startSpan = $(this.options.startSpan || null);
    this.options.endSpan   = $(this.options.endSpan || null);

    this.restricted = this.options.restricted || false;

    this.maximum   = this.options.maximum || this.range.end;
    this.minimum   = this.options.minimum || this.range.start;

    // Will be used to align the handle onto the track, if necessary
    this.alignX = parseInt(this.options.alignX || '0');
    this.alignY = parseInt(this.options.alignY || '0');

    this.trackLength = this.maximumOffset() - this.minimumOffset();

    this.handleLength = this.isVertical() ?
      (this.handles[0].offsetHeight != 0 ?
        this.handles[0].offsetHeight : this.handles[0].style.height.replace(/px$/,"")) :
      (this.handles[0].offsetWidth != 0 ? this.handles[0].offsetWidth :
        this.handles[0].style.width.replace(/px$/,""));

    this.active   = false;
    this.dragging = false;
    this.disabled = false;

    if (this.options.disabled) this.setDisabled();

    // Allowed values array
    this.allowedValues = this.options.values ? this.options.values.sortBy(Prototype.K) : false;
    if (this.allowedValues) {
      this.minimum = this.allowedValues.min();
      this.maximum = this.allowedValues.max();
    }

    this.eventMouseDown = this.startDrag.bindAsEventListener(this);
    this.eventMouseUp   = this.endDrag.bindAsEventListener(this);
    this.eventMouseMove = this.update.bindAsEventListener(this);

    // Initialize handles in reverse (make sure first handle is active)
    this.handles.each( function(h,i) {
      i = slider.handles.length-1-i;
      slider.setValue(parseFloat(
        (Object.isArray(slider.options.sliderValue) ?
          slider.options.sliderValue[i] : slider.options.sliderValue) ||
         slider.range.start), i);
      h.makePositioned().observe("mousedown", slider.eventMouseDown);
    });

    this.track.observe("mousedown", this.eventMouseDown);
    document.observe("mouseup", this.eventMouseUp);
    document.observe("mousemove", this.eventMouseMove);

    this.initialized = true;
  },
  dispose: function() {
    var slider = this;
    Event.stopObserving(this.track, "mousedown", this.eventMouseDown);
    Event.stopObserving(document, "mouseup", this.eventMouseUp);
    Event.stopObserving(document, "mousemove", this.eventMouseMove);
    this.handles.each( function(h) {
      Event.stopObserving(h, "mousedown", slider.eventMouseDown);
    });
  },
  setDisabled: function(){
    this.disabled = true;
  },
  setEnabled: function(){
    this.disabled = false;
  },
  getNearestValue: function(value){
    if (this.allowedValues){
      if (value >= this.allowedValues.max()) return(this.allowedValues.max());
      if (value <= this.allowedValues.min()) return(this.allowedValues.min());

      var offset = Math.abs(this.allowedValues[0] - value);
      var newValue = this.allowedValues[0];
      this.allowedValues.each( function(v) {
        var currentOffset = Math.abs(v - value);
        if (currentOffset <= offset){
          newValue = v;
          offset = currentOffset;
        }
      });
      return newValue;
    }
    if (value > this.range.end) return this.range.end;
    if (value < this.range.start) return this.range.start;
    return value;
  },
  setValue: function(sliderValue, handleIdx){
    if (!this.active) {
      this.activeHandleIdx = handleIdx || 0;
      this.activeHandle    = this.handles[this.activeHandleIdx];
      this.updateStyles();
    }
    handleIdx = handleIdx || this.activeHandleIdx || 0;
    if (this.initialized && this.restricted) {
      if ((handleIdx>0) && (sliderValue<this.values[handleIdx-1]))
        sliderValue = this.values[handleIdx-1];
      if ((handleIdx < (this.handles.length-1)) && (sliderValue>this.values[handleIdx+1]))
        sliderValue = this.values[handleIdx+1];
    }
    sliderValue = this.getNearestValue(sliderValue);
    this.values[handleIdx] = sliderValue;
    this.value = this.values[0]; // assure backwards compat

    this.handles[handleIdx].style[this.isVertical() ? 'top' : 'left'] =
      this.translateToPx(sliderValue);

    this.drawSpans();
    if (!this.dragging || !this.event) this.updateFinished();
  },
  setValueBy: function(delta, handleIdx) {
    this.setValue(this.values[handleIdx || this.activeHandleIdx || 0] + delta,
      handleIdx || this.activeHandleIdx || 0);
  },
  translateToPx: function(value) {
    return Math.round(
      ((this.trackLength-this.handleLength)/(this.range.end-this.range.start)) *
      (value - this.range.start)) + "px";
  },
  translateToValue: function(offset) {
    return ((offset/(this.trackLength-this.handleLength) *
      (this.range.end-this.range.start)) + this.range.start);
  },
  getRange: function(range) {
    var v = this.values.sortBy(Prototype.K);
    range = range || 0;
    return $R(v[range],v[range+1]);
  },
  minimumOffset: function(){
    return(this.isVertical() ? this.alignY : this.alignX);
  },
  maximumOffset: function(){
    return(this.isVertical() ?
      (this.track.offsetHeight != 0 ? this.track.offsetHeight :
        this.track.style.height.replace(/px$/,"")) - this.alignY :
      (this.track.offsetWidth != 0 ? this.track.offsetWidth :
        this.track.style.width.replace(/px$/,"")) - this.alignX);
  },
  isVertical:  function(){
    return (this.axis == 'vertical');
  },
  drawSpans: function() {
    var slider = this;
    if (this.spans)
      $R(0, this.spans.length-1).each(function(r) { slider.setSpan(slider.spans[r], slider.getRange(r)) });
    if (this.options.startSpan)
      this.setSpan(this.options.startSpan,
        $R(0, this.values.length>1 ? this.getRange(0).min() : this.value ));
    if (this.options.endSpan)
      this.setSpan(this.options.endSpan,
        $R(this.values.length>1 ? this.getRange(this.spans.length-1).max() : this.value, this.maximum));
  },
  setSpan: function(span, range) {
    if (this.isVertical()) {
      span.style.top = this.translateToPx(range.start);
      span.style.height = this.translateToPx(range.end - range.start + this.range.start);
    } else {
      span.style.left = this.translateToPx(range.start);
      span.style.width = this.translateToPx(range.end - range.start + this.range.start);
    }
  },
  updateStyles: function() {
    this.handles.each( function(h){ Element.removeClassName(h, 'selected') });
    Element.addClassName(this.activeHandle, 'selected');
  },
  startDrag: function(event) {
    if (Event.isLeftClick(event)) {
      if (!this.disabled){
        this.active = true;

        var handle = Event.element(event);
        var pointer  = [Event.pointerX(event), Event.pointerY(event)];
        var track = handle;
        if (track==this.track) {
          var offsets  = Position.cumulativeOffset(this.track);
          this.event = event;
          this.setValue(this.translateToValue(
           (this.isVertical() ? pointer[1]-offsets[1] : pointer[0]-offsets[0])-(this.handleLength/2)
          ));
          var offsets  = Position.cumulativeOffset(this.activeHandle);
          this.offsetX = (pointer[0] - offsets[0]);
          this.offsetY = (pointer[1] - offsets[1]);
        } else {
          // find the handle (prevents issues with Safari)
          while((this.handles.indexOf(handle) == -1) && handle.parentNode)
            handle = handle.parentNode;

          if (this.handles.indexOf(handle)!=-1) {
            this.activeHandle    = handle;
            this.activeHandleIdx = this.handles.indexOf(this.activeHandle);
            this.updateStyles();

            var offsets  = Position.cumulativeOffset(this.activeHandle);
            this.offsetX = (pointer[0] - offsets[0]);
            this.offsetY = (pointer[1] - offsets[1]);
          }
        }
      }
      Event.stop(event);
    }
  },
  update: function(event) {
   if (this.active) {
      if (!this.dragging) this.dragging = true;
      this.draw(event);
      if (Prototype.Browser.WebKit) window.scrollBy(0,0);
      Event.stop(event);
   }
  },
  draw: function(event) {
    var pointer = [Event.pointerX(event), Event.pointerY(event)];
    var offsets = Position.cumulativeOffset(this.track);
    pointer[0] -= this.offsetX + offsets[0];
    pointer[1] -= this.offsetY + offsets[1];
    this.event = event;
    this.setValue(this.translateToValue( this.isVertical() ? pointer[1] : pointer[0] ));
    if (this.initialized && this.options.onSlide)
      this.options.onSlide(this.values.length>1 ? this.values : this.value, this);
  },
  endDrag: function(event) {
    if (this.active && this.dragging) {
      this.finishDrag(event, true);
      Event.stop(event);
    }
    this.active = false;
    this.dragging = false;
  },
  finishDrag: function(event, success) {
    this.active = false;
    this.dragging = false;
    this.updateFinished();
  },
  updateFinished: function() {
    if (this.initialized && this.options.onChange)
      this.options.onChange(this.values.length>1 ? this.values : this.value, this);
    this.event = null;
  }
});// script.aculo.us sound.js v1.8.2, Tue Nov 18 18:30:58 +0100 2008

// Copyright (c) 2005-2008 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us)
//
// Based on code created by Jules Gravinese (http://www.webveteran.com/)
//
// script.aculo.us is freely distributable under the terms of an MIT-style license.
// For details, see the script.aculo.us web site: http://script.aculo.us/

Sound = {
  tracks: {},
  _enabled: true,
  template:
    new Template('<embed style="height:0" id="sound_#{track}_#{id}" src="#{url}" loop="false" autostart="true" hidden="true"/>'),
  enable: function(){
    Sound._enabled = true;
  },
  disable: function(){
    Sound._enabled = false;
  },
  play: function(url){
    if(!Sound._enabled) return;
    var options = Object.extend({
      track: 'global', url: url, replace: false
    }, arguments[1] || {});

    if(options.replace && this.tracks[options.track]) {
      $R(0, this.tracks[options.track].id).each(function(id){
        var sound = $('sound_'+options.track+'_'+id);
        sound.Stop && sound.Stop();
        sound.remove();
      });
      this.tracks[options.track] = null;
    }

    if(!this.tracks[options.track])
      this.tracks[options.track] = { id: 0 };
    else
      this.tracks[options.track].id++;

    options.id = this.tracks[options.track].id;
    $$('body')[0].insert(
      Prototype.Browser.IE ? new Element('bgsound',{
        id: 'sound_'+options.track+'_'+options.id,
        src: options.url, loop: 1, autostart: true
      }) : Sound.template.evaluate(options));
  }
};

if(Prototype.Browser.Gecko && navigator.userAgent.indexOf("Win") > 0){
  if(navigator.plugins && $A(navigator.plugins).detect(function(p){ return p.name.indexOf('QuickTime') != -1 }))
    Sound.template = new Template('<object id="sound_#{track}_#{id}" width="0" height="0" type="audio/mpeg" data="#{url}"/>');
  else
    Sound.play = function(){};
}/* SWFObject v2.1 <http://code.google.com/p/swfobject/>
	Copyright (c) 2007-2008 Geoff Stearns, Michael Williams, and Bobby van der Sluis
	This software is released under the MIT License <http://www.opensource.org/licenses/mit-license.php>
*/
var swfobject=function(){var b="undefined",Q="object",n="Shockwave Flash",p="ShockwaveFlash.ShockwaveFlash",P="application/x-shockwave-flash",m="SWFObjectExprInst",j=window,K=document,T=navigator,o=[],N=[],i=[],d=[],J,Z=null,M=null,l=null,e=false,A=false;var h=function(){var v=typeof K.getElementById!=b&&typeof K.getElementsByTagName!=b&&typeof K.createElement!=b,AC=[0,0,0],x=null;if(typeof T.plugins!=b&&typeof T.plugins[n]==Q){x=T.plugins[n].description;if(x&&!(typeof T.mimeTypes!=b&&T.mimeTypes[P]&&!T.mimeTypes[P].enabledPlugin)){x=x.replace(/^.*\s+(\S+\s+\S+$)/,"$1");AC[0]=parseInt(x.replace(/^(.*)\..*$/,"$1"),10);AC[1]=parseInt(x.replace(/^.*\.(.*)\s.*$/,"$1"),10);AC[2]=/r/.test(x)?parseInt(x.replace(/^.*r(.*)$/,"$1"),10):0}}else{if(typeof j.ActiveXObject!=b){var y=null,AB=false;try{y=new ActiveXObject(p+".7")}catch(t){try{y=new ActiveXObject(p+".6");AC=[6,0,21];y.AllowScriptAccess="always"}catch(t){if(AC[0]==6){AB=true}}if(!AB){try{y=new ActiveXObject(p)}catch(t){}}}if(!AB&&y){try{x=y.GetVariable("$version");if(x){x=x.split(" ")[1].split(",");AC=[parseInt(x[0],10),parseInt(x[1],10),parseInt(x[2],10)]}}catch(t){}}}}var AD=T.userAgent.toLowerCase(),r=T.platform.toLowerCase(),AA=/webkit/.test(AD)?parseFloat(AD.replace(/^.*webkit\/(\d+(\.\d+)?).*$/,"$1")):false,q=false,z=r?/win/.test(r):/win/.test(AD),w=r?/mac/.test(r):/mac/.test(AD);/*@cc_on q=true;@if(@_win32)z=true;@elif(@_mac)w=true;@end@*/return{w3cdom:v,pv:AC,webkit:AA,ie:q,win:z,mac:w}}();var L=function(){if(!h.w3cdom){return }f(H);if(h.ie&&h.win){try{K.write("<script id=__ie_ondomload defer=true src=//:><\/script>");J=C("__ie_ondomload");if(J){I(J,"onreadystatechange",S)}}catch(q){}}if(h.webkit&&typeof K.readyState!=b){Z=setInterval(function(){if(/loaded|complete/.test(K.readyState)){E()}},10)}if(typeof K.addEventListener!=b){K.addEventListener("DOMContentLoaded",E,null)}R(E)}();function S(){if(J.readyState=="complete"){J.parentNode.removeChild(J);E()}}function E(){if(e){return }if(h.ie&&h.win){var v=a("span");try{var u=K.getElementsByTagName("body")[0].appendChild(v);u.parentNode.removeChild(u)}catch(w){return }}e=true;if(Z){clearInterval(Z);Z=null}var q=o.length;for(var r=0;r<q;r++){o[r]()}}function f(q){if(e){q()}else{o[o.length]=q}}function R(r){if(typeof j.addEventListener!=b){j.addEventListener("load",r,false)}else{if(typeof K.addEventListener!=b){K.addEventListener("load",r,false)}else{if(typeof j.attachEvent!=b){I(j,"onload",r)}else{if(typeof j.onload=="function"){var q=j.onload;j.onload=function(){q();r()}}else{j.onload=r}}}}}function H(){var t=N.length;for(var q=0;q<t;q++){var u=N[q].id;if(h.pv[0]>0){var r=C(u);if(r){N[q].width=r.getAttribute("width")?r.getAttribute("width"):"0";N[q].height=r.getAttribute("height")?r.getAttribute("height"):"0";if(c(N[q].swfVersion)){if(h.webkit&&h.webkit<312){Y(r)}W(u,true)}else{if(N[q].expressInstall&&!A&&c("6.0.65")&&(h.win||h.mac)){k(N[q])}else{O(r)}}}}else{W(u,true)}}}function Y(t){var q=t.getElementsByTagName(Q)[0];if(q){var w=a("embed"),y=q.attributes;if(y){var v=y.length;for(var u=0;u<v;u++){if(y[u].nodeName=="DATA"){w.setAttribute("src",y[u].nodeValue)}else{w.setAttribute(y[u].nodeName,y[u].nodeValue)}}}var x=q.childNodes;if(x){var z=x.length;for(var r=0;r<z;r++){if(x[r].nodeType==1&&x[r].nodeName=="PARAM"){w.setAttribute(x[r].getAttribute("name"),x[r].getAttribute("value"))}}}t.parentNode.replaceChild(w,t)}}function k(w){A=true;var u=C(w.id);if(u){if(w.altContentId){var y=C(w.altContentId);if(y){M=y;l=w.altContentId}}else{M=G(u)}if(!(/%$/.test(w.width))&&parseInt(w.width,10)<310){w.width="310"}if(!(/%$/.test(w.height))&&parseInt(w.height,10)<137){w.height="137"}K.title=K.title.slice(0,47)+" - Flash Player Installation";var z=h.ie&&h.win?"ActiveX":"PlugIn",q=K.title,r="MMredirectURL="+j.location+"&MMplayerType="+z+"&MMdoctitle="+q,x=w.id;if(h.ie&&h.win&&u.readyState!=4){var t=a("div");x+="SWFObjectNew";t.setAttribute("id",x);u.parentNode.insertBefore(t,u);u.style.display="none";var v=function(){u.parentNode.removeChild(u)};I(j,"onload",v)}U({data:w.expressInstall,id:m,width:w.width,height:w.height},{flashvars:r},x)}}function O(t){if(h.ie&&h.win&&t.readyState!=4){var r=a("div");t.parentNode.insertBefore(r,t);r.parentNode.replaceChild(G(t),r);t.style.display="none";var q=function(){t.parentNode.removeChild(t)};I(j,"onload",q)}else{t.parentNode.replaceChild(G(t),t)}}function G(v){var u=a("div");if(h.win&&h.ie){u.innerHTML=v.innerHTML}else{var r=v.getElementsByTagName(Q)[0];if(r){var w=r.childNodes;if(w){var q=w.length;for(var t=0;t<q;t++){if(!(w[t].nodeType==1&&w[t].nodeName=="PARAM")&&!(w[t].nodeType==8)){u.appendChild(w[t].cloneNode(true))}}}}}return u}function U(AG,AE,t){var q,v=C(t);if(v){if(typeof AG.id==b){AG.id=t}if(h.ie&&h.win){var AF="";for(var AB in AG){if(AG[AB]!=Object.prototype[AB]){if(AB.toLowerCase()=="data"){AE.movie=AG[AB]}else{if(AB.toLowerCase()=="styleclass"){AF+=' class="'+AG[AB]+'"'}else{if(AB.toLowerCase()!="classid"){AF+=" "+AB+'="'+AG[AB]+'"'}}}}}var AD="";for(var AA in AE){if(AE[AA]!=Object.prototype[AA]){AD+='<param name="'+AA+'" value="'+AE[AA]+'" />'}}v.outerHTML='<object classid="clsid:D27CDB6E-AE6D-11cf-96B8-444553540000"'+AF+">"+AD+"</object>";i[i.length]=AG.id;q=C(AG.id)}else{if(h.webkit&&h.webkit<312){var AC=a("embed");AC.setAttribute("type",P);for(var z in AG){if(AG[z]!=Object.prototype[z]){if(z.toLowerCase()=="data"){AC.setAttribute("src",AG[z])}else{if(z.toLowerCase()=="styleclass"){AC.setAttribute("class",AG[z])}else{if(z.toLowerCase()!="classid"){AC.setAttribute(z,AG[z])}}}}}for(var y in AE){if(AE[y]!=Object.prototype[y]){if(y.toLowerCase()!="movie"){AC.setAttribute(y,AE[y])}}}v.parentNode.replaceChild(AC,v);q=AC}else{var u=a(Q);u.setAttribute("type",P);for(var x in AG){if(AG[x]!=Object.prototype[x]){if(x.toLowerCase()=="styleclass"){u.setAttribute("class",AG[x])}else{if(x.toLowerCase()!="classid"){u.setAttribute(x,AG[x])}}}}for(var w in AE){if(AE[w]!=Object.prototype[w]&&w.toLowerCase()!="movie"){F(u,w,AE[w])}}v.parentNode.replaceChild(u,v);q=u}}}return q}function F(t,q,r){var u=a("param");u.setAttribute("name",q);u.setAttribute("value",r);t.appendChild(u)}function X(r){var q=C(r);if(q&&(q.nodeName=="OBJECT"||q.nodeName=="EMBED")){if(h.ie&&h.win){if(q.readyState==4){B(r)}else{j.attachEvent("onload",function(){B(r)})}}else{q.parentNode.removeChild(q)}}}function B(t){var r=C(t);if(r){for(var q in r){if(typeof r[q]=="function"){r[q]=null}}r.parentNode.removeChild(r)}}function C(t){var q=null;try{q=K.getElementById(t)}catch(r){}return q}function a(q){return K.createElement(q)}function I(t,q,r){t.attachEvent(q,r);d[d.length]=[t,q,r]}function c(t){var r=h.pv,q=t.split(".");q[0]=parseInt(q[0],10);q[1]=parseInt(q[1],10)||0;q[2]=parseInt(q[2],10)||0;return(r[0]>q[0]||(r[0]==q[0]&&r[1]>q[1])||(r[0]==q[0]&&r[1]==q[1]&&r[2]>=q[2]))?true:false}function V(v,r){if(h.ie&&h.mac){return }var u=K.getElementsByTagName("head")[0],t=a("style");t.setAttribute("type","text/css");t.setAttribute("media","screen");if(!(h.ie&&h.win)&&typeof K.createTextNode!=b){t.appendChild(K.createTextNode(v+" {"+r+"}"))}u.appendChild(t);if(h.ie&&h.win&&typeof K.styleSheets!=b&&K.styleSheets.length>0){var q=K.styleSheets[K.styleSheets.length-1];if(typeof q.addRule==Q){q.addRule(v,r)}}}function W(t,q){var r=q?"visible":"hidden";if(e&&C(t)){C(t).style.visibility=r}else{V("#"+t,"visibility:"+r)}}function g(s){var r=/[\\\"<>\.;]/;var q=r.exec(s)!=null;return q?encodeURIComponent(s):s}var D=function(){if(h.ie&&h.win){window.attachEvent("onunload",function(){var w=d.length;for(var v=0;v<w;v++){d[v][0].detachEvent(d[v][1],d[v][2])}var t=i.length;for(var u=0;u<t;u++){X(i[u])}for(var r in h){h[r]=null}h=null;for(var q in swfobject){swfobject[q]=null}swfobject=null})}}();return{registerObject:function(u,q,t){if(!h.w3cdom||!u||!q){return }var r={};r.id=u;r.swfVersion=q;r.expressInstall=t?t:false;N[N.length]=r;W(u,false)},getObjectById:function(v){var q=null;if(h.w3cdom){var t=C(v);if(t){var u=t.getElementsByTagName(Q)[0];if(!u||(u&&typeof t.SetVariable!=b)){q=t}else{if(typeof u.SetVariable!=b){q=u}}}}return q},embedSWF:function(x,AE,AB,AD,q,w,r,z,AC){if(!h.w3cdom||!x||!AE||!AB||!AD||!q){return }AB+="";AD+="";if(c(q)){W(AE,false);var AA={};if(AC&&typeof AC===Q){for(var v in AC){if(AC[v]!=Object.prototype[v]){AA[v]=AC[v]}}}AA.data=x;AA.width=AB;AA.height=AD;var y={};if(z&&typeof z===Q){for(var u in z){if(z[u]!=Object.prototype[u]){y[u]=z[u]}}}if(r&&typeof r===Q){for(var t in r){if(r[t]!=Object.prototype[t]){if(typeof y.flashvars!=b){y.flashvars+="&"+t+"="+r[t]}else{y.flashvars=t+"="+r[t]}}}}f(function(){U(AA,y,AE);if(AA.id==AE){W(AE,true)}})}else{if(w&&!A&&c("6.0.65")&&(h.win||h.mac)){A=true;W(AE,false);f(function(){var AF={};AF.id=AF.altContentId=AE;AF.width=AB;AF.height=AD;AF.expressInstall=w;k(AF)})}}},getFlashPlayerVersion:function(){return{major:h.pv[0],minor:h.pv[1],release:h.pv[2]}},hasFlashPlayerVersion:c,createSWF:function(t,r,q){if(h.w3cdom){return U(t,r,q)}else{return undefined}},removeSWF:function(q){if(h.w3cdom){X(q)}},createCSS:function(r,q){if(h.w3cdom){V(r,q)}},addDomLoadEvent:f,addLoadEvent:R,getQueryParamValue:function(v){var u=K.location.search||K.location.hash;if(v==null){return g(u)}if(u){var t=u.substring(1).split("&");for(var r=0;r<t.length;r++){if(t[r].substring(0,t[r].indexOf("="))==v){return g(t[r].substring((t[r].indexOf("=")+1)))}}}return""},expressInstallCallback:function(){if(A&&M){var q=C(m);if(q){q.parentNode.replaceChild(M,q);if(l){W(l,true);if(h.ie&&h.win){M.style.display="block"}}M=null;l=null;A=false}}}}}();/*
	CSS Browser Selector v0.2.9
	Rafael Lima (http://rafael.adm.br)
	http://rafael.adm.br/css_browser_selector
	License: http://creativecommons.org/licenses/by/2.5/
	Contributors: http://rafael.adm.br/css_browser_selector#contributors
*/
var css_browser_selector = function() {var ua=navigator.userAgent.toLowerCase(),is=function(t){return ua.indexOf(t) != -1;},h=document.getElementsByTagName('html')[0],b=(!(/opera|webtv/i.test(ua))&&/msie\s(\d)/.test(ua))?('ie ie'+RegExp.$1):is('firefox/2')?'gecko ff2':is('firefox/3')?'gecko ff3':is('gecko/')?'gecko':is('opera/9')?'opera opera9':/opera\s(\d)/.test(ua)?'opera opera'+RegExp.$1:is('konqueror')?'konqueror':is('chrome')?'chrome webkit safari':is('applewebkit/')?'webkit safari':is('mozilla/')?'gecko':'',os=(is('x11')||is('linux'))?' linux':is('mac')?' mac':is('win')?' win':'';var c=b+os+' js'; h.className += h.className?' '+c:c;}();
 

/**
 * @author Ryan Johnson <ryan@livepipe.net>
 * @copyright 2007 LivePipe LLC
 * @package Control.Tabs
 * @license MIT
 * @url http://livepipe.net/projects/control_tabs/
 * @version 2.1.1
 */

if(typeof(Control) == 'undefined')
	var Control = {};
Control.Tabs = Class.create();
Object.extend(Control.Tabs,{
	instances: [],
	findByTabId: function(id){
		return Control.Tabs.instances.find(function(tab){
			return tab.links.find(function(link){
				return link.key == id;
			});
		});
	}
});
Object.extend(Control.Tabs.prototype,{
	initialize: function(tab_list_container,options){
		this.activeContainer = false;
		this.activeLink = false;
		this.containers = $H({});
		this.links = [];
		Control.Tabs.instances.push(this);
		this.options = {
			beforeChange: Prototype.emptyFunction,
			afterChange: Prototype.emptyFunction,
			hover: false,
			linkSelector: 'li a',
			setClassOnContainer: false,
			activeClassName: 'active',
			defaultTab: 'first',
			autoLinkExternal: true,
			targetRegExp: /#(.+)$/,
			showFunction: Element.show,
			hideFunction: Element.hide
		};
		Object.extend(this.options,options || {});
		(typeof(this.options.linkSelector == 'string')
			? $(tab_list_container).getElementsBySelector(this.options.linkSelector)
			: this.options.linkSelector($(tab_list_container))
		).findAll(function(link){
			return (/^#/).exec(link.href.replace(window.location.href.split('#')[0],''));
		}).each(function(link){
			this.addTab(link);
		}.bind(this));
		this.containers.values().each(this.options.hideFunction);
		if(this.options.defaultTab == 'first')
			this.setActiveTab(this.links.first());
		else if(this.options.defaultTab == 'last')
			this.setActiveTab(this.links.last());
		else
			this.setActiveTab(this.options.defaultTab);
		var targets = this.options.targetRegExp.exec(window.location);
		if(targets && targets[1]){
			targets[1].split(',').each(function(target){
				this.links.each(function(target,link){
					if(link.key == target){
						this.setActiveTab(link);
						throw $break;
					}
				}.bind(this,target));
			}.bind(this));
		}
		if(this.options.autoLinkExternal){
			$A(document.getElementsByTagName('a')).each(function(a){
				if(!this.links.include(a)){
					var clean_href = a.href.replace(window.location.href.split('#')[0],'');
					if(clean_href.substring(0,1) == '#'){
						if(this.containers.keys().include(clean_href.substring(1))){
							$(a).observe('click',function(event,clean_href){
								this.setActiveTab(clean_href.substring(1));
							}.bindAsEventListener(this,clean_href));
						}
					}
				}
			}.bind(this));
		}
	},
	addTab: function(link){
		this.links.push(link);
		link.key = link.getAttribute('href').replace(window.location.href.split('#')[0],'').split('/').last().replace(/#/,'');
		this.containers[link.key] = $(link.key);
		link[this.options.hover ? 'onmouseover' : 'onclick'] = function(link){
			if(window.event)
				Event.stop(window.event);
			this.setActiveTab(link);
			return false;
		}.bind(this,link);
	},
	setActiveTab: function(link){
		if(!link)
			return;
		if(typeof(link) == 'string'){
			this.links.each(function(_link){
				if(_link.key == link){
					this.setActiveTab(_link);
					throw $break;
				}
			}.bind(this));
		}else{
			this.notify('beforeChange',this.activeContainer);
			if(this.activeContainer)
				this.options.hideFunction(this.activeContainer);
			this.links.each(function(item){
				(this.options.setClassOnContainer ? $(item.parentNode) : item).removeClassName(this.options.activeClassName);
			}.bind(this));
			(this.options.setClassOnContainer ? $(link.parentNode) : link).addClassName(this.options.activeClassName);
			this.activeContainer = this.containers[link.key];
			this.activeLink = link;
			this.options.showFunction(this.containers[link.key]);
			this.notify('afterChange',this.containers[link.key]);
		}
	},
	next: function(){
		this.links.each(function(link,i){
			if(this.activeLink == link && this.links[i + 1]){
				this.setActiveTab(this.links[i + 1]);
				throw $break;
			}
		}.bind(this));
		return false;
	},
	previous: function(){
		this.links.each(function(link,i){
			if(this.activeLink == link && this.links[i - 1]){
				this.setActiveTab(this.links[i - 1]);
				throw $break;
			}
		}.bind(this));
		return false;
	},
	first: function(){
		this.setActiveTab(this.links.first());
		return false;
	},
	last: function(){
		this.setActiveTab(this.links.last());
		return false;
	},
	notify: function(event_name){
		try{
			if(this.options[event_name])
				return [this.options[event_name].apply(this.options[event_name],$A(arguments).slice(1))];
		}catch(e){
			if(e != $break)
				throw e;
			else
				return false;
		}
	}
});
if(typeof(Object.Event) != 'undefined')
	Object.Event.extend(Control.Tabs);generic = {     
    init: function() {  
        this.env.debug = this.env.query("debug"); 
        if (this.env.debug && this.env.isIE) console.drawWin(); 
    },
    env: { 
        isIE : !!(typeof(ActiveXObject) == 'function'),
        isIE6 : !!(!!(typeof(ActiveXObject) == 'function') && (/MSIE\s6\.0/.test(navigator.appVersion))),
        isFF : Prototype.Browser.Gecko,
        isFF2 : !!(typeof(navigator.product) != 'undefined' && navigator.product == 'Gecko' && !((document.childNodes) && (!navigator.taintEnabled)) && navigator.userAgent.toLowerCase().split(' firefox/')[1].split('.')[0] == '2'),
        isFF3 : !!(typeof(navigator.product) != 'undefined' && navigator.product == 'Gecko' && !((document.childNodes) && (!navigator.taintEnabled)) && navigator.userAgent.toLowerCase().split(' firefox/')[1].split('.')[0] == '3'),
        isMac    : !!(/macppc|macintel/.test(navigator.platform.toLowerCase())),
        isSafari : !!(/Safari/.test(navigator.userAgent)),
        
        domain : window.location.protocol + "//" + window.location.hostname,
        
        query: function(key) {
            if (typeof generic.env.parsedQuery == "undefined") {
                generic.env.parsedQuery = window.location.href.toQueryParams();
            }
            var result = generic.env.parsedQuery[key] || null;
            return result; 
        },
        
        debug: false
        
    }, 
    helpers: { 
        div: new Element("div") //used by Widget class, Prototype way is needed for IE
    },
    events: {
        target: document,
        fire: function(args) {
            //console.log("generic.events.fire: "+args.event + " / " + args.msg);  
            if (!args) return;
            var e = args.event; 
            var msg = (typeof args.msg == "undefined") ? null : args.msg; 
            generic.events.target.fire(e, {msg:msg});
        },
        observe: function(evt, func) {
            if (!evt || !func) return; 
            generic.events.target.observe(evt, function(e){   
                func(e.memo.msg);
            });
        }   
    },
    forms: {
        select: {
            addOption:  function(args) {
                if (!args || !args.menuNode) return;
                var val = args.value;
                var label = args.label || val;
                var options = args.menuNode.options;
                options[options.length] = new Option(label, val);
            },
            setValue: function(args) {
                var idx = 0;
                for (var i=0, len=args.menuNode.options.length; i<len; i++) {
                    if (args.value == args.menuNode.options[i].value) {
                        idx = i;
                        break;
                    }
                }
                args.menuNode.selectedIndex = idx;
            }
        }
    }
}; 

    
// debug    
if (typeof console === "undefined") {
    if (generic && generic.env.debug) {
        console = {
            tracen : 0,
            win : {},
            drawWin: function() {  
                outp = document.createElement("DIV");
                outp.id = "console-window";
                outp.style.cssText = "position:absolute;top:10px;right:10px;width:400px;height:200px;padding:5px;overflow-x:hidden;overflow-y:scroll;background-color:#ffffff;color:#000000;font-size:12px;border:1px solid red;z-index:9999";
                document.body.appendChild(outp);  
                this.win = $(outp.id);
            },
            log: function(s) {
                if ((typeof this.win != "undefined") && (typeof this.win.innerHTML != "undefined")) {
                    this.tracen++;
                    s = (typeof(s) == "undefined") ? "undefined" : s.toString().replace(/\</gi, "&lt;").replace(/\>/gi, "&gt;");
                    this.win.innerHTML = this.win.innerHTML + "<b>" + this.tracen + "</b>. " + s + "<br/>";
                }
            }    
        }   
    } else {
        console = {
            tracen : 0,
            win : {},
            drawWin: function() {  
                return;
            },
            log: function(s) {
                return;
            }    
        }
    }
};
var generic = generic || {};
generic.rb = generic.rb || {};
var rb = rb || {};

/**
	* This method provides access to resource bundle values that have been 
	* written to the HTML in JSON format. The file that outputs these values
	* must be included in the .html as a script tag with the desired RB name
	* as a query string paramter.
	* It returns an object that exposes one method: get()
	* @example
	* // var myBundle = generic.rb("account");
	* // myBundle.get("err_please_sign_in");
	* @example
    * <script src="/js/shared/v2/internal/resource.tmpl?rb=account"></script>
	* @param {String} rbGroupName name of resource bundle needed
	* @methodOf generic
*/
generic.rb = function(rbGroupName) {
	var findResourceBundle = function(groupName) {
		if (groupName && rb) {
			var rbName = groupName;
			var rbHash = $H(rb[rbName]);
			if (rbHash) {
				return rbHash;
			} else {
				return $H({});
			}
		} else {
			return $H({});
		}
	};
	
	var resourceBundle = findResourceBundle(rbGroupName);
	
	var returnObj = {
    	/**
        * This method will return the value for the requested Resource Bundle key.
        * If the key is not found, the key name will be returned.
    	* @example
    	* // var myBundle = generic.rb("account");
    	* // myBundle.get("err_please_sign_in");
    	* @param {String} keyName key of desired Resource Bundle value
    	*/
		get: function(keyName) {
		    if ( ! Object.isString(keyName) ) {
		        return null;
		    }
			var val = resourceBundle.get(keyName);
			if (val) {
				return val;
			} else {
				return keyName;
			}
		}
	};
	
	return returnObj;
	
};var generic = generic || {};

generic.RediTemplate = Class.create( Template, {
    initialize: function ( template, pattern ) {
        this.template = template?template:'';
        this.readyState = template?1:0;
        this.pattern = pattern?pattern:Template.Pattern; 
        this.queue = new Array();       
        return;
    },
    load: function(template) {
        this.template = template.toString(); 
        this.readyState = 1; 
        this.onReadyState();
    }, 
    evaluateCallback: function (options) {  
        this.options = {
          object:       {},
          callback:     function () {}
        };
        Object.extend(this.options, options || { });  
        if (this.readyState) {   
            this.options.callback(this.evaluate(this.options.object));
        } else { 
            this.queue.push({
                qtype: 'callback',
                obj: this.options.object,
                fnc: this.options.callback
            });
         }
         return;        
    },
    onReadyState: function () {
        while (q = this.queue.shift()) {
            var object = q.obj;
            var qtype = q.qtype;
            var callback = q.fnc;
            var elm;
            callback(this.evaluate(object)); 
        }
    }
}); 

/*
 WDR:
 Some new options to the get() method:
 
 urlparams -- a js hash of stuff to be added to the query string. 
    simple example:

    generic.templatefactory.get({
        path: '/templates/edit-address-jsdata.tmpl',
        urlparams: {
            ADDRESS_ID: options.addrid
        }
    }).evaluateCallback({
        ...blah blah... 
    });

    this results in the url 'http://(domain)/templates/edit-address-jsdata.tmpl?ADDRESS_ID=88888'
    (assuming 88888 is the value of "options.addrid")
    
    From there, the perl side is plain old $request->param('ADDRESS_ID') or whatever...
    
 method -- HTTP method to use.  default is "GET", but you can override as "POST" if desired.
 
 NOTE - The default get() params use the "GET" http method and no query string.
 This leverages the browser cache to save the gotten file for subsequent calls.
 Therefore, understand that using the urlparams and method options may bypass
 the browser cache, which may or may not be your intent.

*/

generic.TemplateFactory = Class.create( Hash, {  
    templatesHash: false,
    get: function (params) {
        var key = params.key || params.path;
        var query = params.query;
        var forceReload = params.forceReload || false; 
        var templateString = params.templateString || false;

        // RediTemplate previously created
        if (typeof this._object[key] != "undefined" && !forceReload && !query) {  
            return this._object[key];
        }
        
        // create RediTemplate 
        this._object[key] = new generic.RediTemplate();  
        
        // check if the path is a key in a hash
        if (this.templateHash) { 
            try { if (key.indexOf(this.templateHash)==0) templateString = eval(key); } catch(e) {}
        }
        
        // check if using a string (either via params.templateString or via the hash)
        if (templateString) {
            this._object[key].load(templateString);
            return this._object[key];
        } 
        
        // get template via Ajax
        var url = key;
        if (query) {
            var q = $H(query);
            var queryString = q.toQueryString();
            url += "?" + queryString;
        }
        var tAjax = new Ajax.Request(url, {
            method: params.method || 'get',
            parameters: params.urlparams,
            onSuccess: function(transport) {   
                this._object[key].load(transport.responseText);
            }.bind(this)
        });
        return this._object[key];
    }   
});
generic.templatefactory = new generic.TemplateFactory();Widget = Class.create({
    setProperties: function(args) {
        Object.extend(this, args);
    }, 
    initialize: function(o) {  
        if (o) this.setProperties(o);  
        //console.log("Widget.init: "+this.id); 
        this.domNode = false;
        this.children = []; 
        
        if (this.templatePath||this.templateString) {
            this.mixInProperties();
        } else { //widget has no template    
            if ($(this.id))  $(this.id).widget = this; 
            this.create();  
        } 
    },
    mixInProperties: function() { 
        //console.log("this.mixInProperties "+this.id+"/"+this.templatePath);
        var key = (this.templateKey ? this.templateKey : this.templatePath);
        var forceReload = (this.forceReload ? this.forceReload : false);
        var params = {key:key, forceReload:forceReload, query: this.query}; 
        if (this.templateString) params.templateString = this.templateString; 
        generic.templatefactory.get(params).evaluateCallback({
            object: this,
            callback: this.handleMixIn.bind(this)
        }); 
    },
    handleMixIn: function(html) { 
        html = html.strip();  
        generic.helpers.div.update(html); 
        this.domNode = generic.helpers.div.firstDescendant();  
    
        var existingContent = false; 
        if ($(this.id)) this.nodeToReplace = $(this.id);
        if (this.nodeToReplace) { //obj with same id might exist, or a different container passed in to be replaced with this node
            //console.log("handleMixIn: this.nodeToReplace "+this.nodeToReplace.id + " /check: "+$("gnav_products"));
            existingContent = this.nodeToReplace.innerHTML; 
            if (this.reinsertNode) {
                //console.log("this.handleMixIn: this.reinsertNode");
                this.nodeToReplace.parentNode.removeChild(this.nodeToReplace); //e.g. psubnav_my_mac
                this.nodeToReplace = false;
            }
        } 
    
        if (this.nodeToReplace) {
            this.updateMixIn();
        } else {
            this.insertMixIn();
        }
        
        var self = this;
        this.domNode.widget = self;     
        this.attachPoints();  
        this.attachEvents();  
        this.containerNode = this.containerNode ? this.containerNode : this.domNode;
        
        if (existingContent) { 
            this.containerNode.insert(existingContent);
        }
        
        this.create(); 
    },
    updateMixIn: function() { 
        //console.log("Widget.updateMixIn: "+this.domNode+" "+this.nodeToReplace.id + " /check: "+$("gnav_products"));
        this.nodeToReplace.parentNode.replaceChild(this.domNode, this.nodeToReplace); 
    },
    insertMixIn: function() {
        if (this.domInsertionMethod) { 
            this.domInsertionMethod(this);
        } else { 
            try {  
                var container = this.domParent ?  this.domParent : $(this.parentId).widget.containerNode;
                if (typeof container == "string") container = $(container); 
                //console.log("this.handleMixIn "+this.id+" // "+container.id);    
                container.insert(this.domNode);   
            } catch(e) {
                console.log("Widget.insertMixIn e: "+this.id+"/"+this.parentId+" / error: ",e);
            }
        }  
    },
    create: function() {   
        //console.log("Widget.create: "+this.id);   
        if ($(this.parentId) && $(this.parentId).widget) {
            this.parent = $(this.parentId).widget;
            $(this.parentId).widget.children.push(this); 
        } else {
            //console.log("!! Widget.create: "+this.id +" / "+ this.domParent +" / "+ this.parentId +" / "+ $(this.parentId));   
        }       
        
        if (this.postCreate) {
            this.postCreate();   
        } 
    },
    attachPoints: function() { 
        //console.log("Widget.attachPoints "+this.id);    
        var self = this; 
        try {
            var elemsWithPoints = this.domNode.select("[attachPoint]");   
            if (null !== this.domNode.getAttribute("attachPoint")) elemsWithPoints.push(this.domNode);  
            var attachPoint;
        
            elemsWithPoints.each(function(elem) {   
                attachPoints = String(elem.getAttribute("attachPoint")).split(",");  
                attachPoints.each(function(attachPoint){
                    //console.log("Widget.attachPoints: "+elem.id+"/"+attachPoint);  
                    self[attachPoint] = elem; 
                    self["domNode"].widget[attachPoint] = elem;  
                }); 
        
            });
        } catch(e) {
            console.log("Widget.attachPoints e "+ this.domNode + " " +e.description);   
            return;
        }  
    },
    attachEvents: function() { 
        //console.log("Widget.attachEvents: "+this.id);    
        var self = this; 
        try {
            var elemsWithEvents = this.domNode.select("[attachEvent]"); 
            if (null !== this.domNode.getAttribute("attachEvent")) elemsWithEvents.push(this.domNode);  
            var events, etype, functionName, func; 
        
            elemsWithEvents.each(function(elem) { 
                events = String(elem.getAttribute("attachEvent")).split(",");  
                events.each(function(event){
                    event = event.split(":");
                    etype = event[0];
                    functionName = event[1];
                    func = self[functionName];
                    //console.log('Widget.attachEvents: ' + elem.id + '  attached e ' + functionName + ' e type = '+etype+" func = "+func);   
                    //if (func) elem.observe(etype, func.bind(self)); // ie bug
                    if (func) elem["on"+etype] = func.bind(self);
                }); 
            });
        } catch(e) {
            console.log("Widget.attachEvents e "+ this.domNode + " " +e.description);   
            return;
        }  
    }
});var generic = generic || {};

/**
 * This singleton class provides an interface to the Perl Gem JSON-RPC methods via AJAX.
 * @memberOf generic
 */
generic.jsonrpc = ( function() {
    var jsonRpcObj = {

	    id: 0, 
	    url: generic.env.domain + "/rpc/jsonrpc.tmpl",
	    errorCodes: {
	        101: "The data type of this method is not supported.",
	        102: "The data type of the request parameters is not supported.",
	        103: "Your request did not return any results.",
	        104: "Response is not in the expected format."
	    },

		/**
		 * This is the method to use when calling a JSON-RPC method.
		 * @example
		generic.jsonrpc.fetch({
		    method: 'rpc.form',
			"params": [
				{
					"_SUBMIT": "address",
					"COUNTRY_ID": "46",
					"ADDRESS_ID": "9342012",
					"LAST_NAME": "Don",
					"FIRST_NAME": "William"
				}
			]
		    onSuccess: function(jsonRpcResponse) {
		        var responseData = jsonRpcResponse.getData();
				console.dir(responseData);
		    },
		    onFailure: function (jsonRpcResponse) {
		        var errorObjectsArray = jsonRpcResponse.getMessages();
		        var errListNode = addressForm.select("ul.error_messages")[0];
		        generic.showErrors(errorObjectsArray, errListNode, addressForm);
		    }
		}); // end jsonRpcWrapper.fetch
		
         * @param {Array} *Optional* args.params an Array of hashes of method parameters.
         * @param {string|Node} args.method *Optional* name of JSON-RPC method to call. Default value is 'rpc.form'
         * @param {function} args.onSuccess *Optional* callback function. It is invoked with a JsonRpcResponse object as a parameter if the AJAX call returns with HTTP status of 200 AND without a JSON-RPC error.
         * @param {function} args.onFailure *Optional* callback function. It is invoked with a JsonRpcResponse object if the AJAX call returns with HTTP status other than 200. It is also invoked if a 200 response contains a JSON-RPC error.
         * @param {function} args.onBoth *Optional* callback function. If provided, it will override any other callback function passed as a parameter and it will be invoked by any JSON-RPC response.
		 * @memberOf generic.jsonrpc
		 */
	    fetch: function(/* Object*/args) {
	        var self = this;
	        this.id++;

	        var options = {method:'post'};

	        if (args.onBoth) {
	            options.onSuccess = args.onBoth;
	            options.onFailure = args.onBoth;
	        } else {
	            options.onSuccess = args.onSuccess || function (response) {
	                console.log('JSON-RPC success');
	                console.log(Object.toJSON(response.getValue()));
	            };
	            options.onFailure = args.onFailure || function (response) {
	                console.log('JSON-RPC failure');
	                console.log(Object.toJSON(response.getMessages()));
	            };
	        }

	        options.onSuccess = options.onSuccess.wrap(
	            function(proceed, response) {
	                if (!response||!response.responseText) { // empty response
	                    errorHandler(this.createErrorResponse(103));
	                    return;
	                }
	                // Analytics general event for RPC..
	                generic.events.fire({event:'RPC:RESULT',msg:response});

	                var responseArray = response.responseText.evalJSON(true);

	                if (Object.isArray(responseArray)) {
	                    var resultObj = responseArray[0];
	                    if (resultObj) {
	                        var jsonRpcResponse = generic.jsonRpcResponse(resultObj);
	                        if (resultObj.error) { // server returns an error
	                            errorHandler(jsonRpcResponse);
	                        } else if (resultObj.result) { // successful response in expected format
	                            //console.log("generic.jsonrpc.onSuccess");
	                            proceed(jsonRpcResponse);
	                        }
	                    } else { // top-level response array is empty
	                        errorHandler(self.createErrorResponse(103));
	                    }
	                } else { // response is not in expected format (array) 
	                    errorHandler(self.createErrorResponse(104));
	                }
	            });

	        options.onFailure = options.onFailure.wrap( function(proceed, response) {
                var resp = response;
                //server returned failure, i.e. onFailure was not triggered by this class
                if (typeof response.responseText != "undefined") {
                    //console.log("generic.jsonRPC onFailure: server error");
                    try { //server returns an error in json
                        var responseArray = response.responseText.evalJSON(true);
                        var resultObj = responseArray[0];
                        resp = generic.jsonRpcResponse(resultObj);
                    } catch(e) { //server response is not json
                        //console.log("generic.jsonRPC onFailure: server error, result is not json");
                        resp = self.createErrorResponse(response.status,response.responseText);
                    }
                }

                proceed(resp);
            });

	        var errorHandler = options.onFailure;
	        var method = args.method || 'rpc.form';
	        var params = args.params || [];

	        var postObj = {
	            method: method,
	            id: self.id
	        };

	        // make sure a method was passed
	        if ( !Object.isString(method) || method.length <= 0 ) {
	            errorHandler(self.createErrorResponse(101));
	            return null;
	        }

	        //make sure that the params type is an obj
	        if (typeof params === 'string') {
	            postObj.params = params.evalJSON();
	        } else if (typeof params === 'object') {
	            postObj.params = params;
	        } else {
	            errorHandler(self.createErrorResponse(102));
	            return null;
	        }

	        var postString = "[" + Object.toJSON(postObj) + "]";

	        options.parameters = $H({JSONRPC:[postString]}).toQueryString();
	        var url = this.url + '?dbgmethod=' + method;
	        new Ajax.Request( url, options );
	        return this.id;
	    },

	    createErrorResponse: function(errorCode, errorMsg) {
	        errorMsg = errorMsg || this.errorCodes[errorCode];
	        var errorObj = new generic.jsonRpcResponse({
	            "error" : {
	                "code": errorCode,
	                "data": {
	                "messages" : [{
	                    "text" : errorMsg,
	                    "display_locations" : [],
	                    "severity" : "MESSAGE",
	                    "tags" : [],
	                    "key" : ""
	                }]
	                }
	            },
	            "id" : this.id
	        });
	        return errorObj;
	    }
	};
    return jsonRpcObj;
} )(); 


 /**
 * A JsonRpcResponse object is instantiated and returned to the onSuccess and onError
 * callback functions that are passed to the fetch() method. It exposes the contents
 * of the response through its getData, getError, and getMessages methods.
 */
generic.jsonRpcResponse = function (resultObj) {
	var jsonRpcResponseObj = {};
    var rawResponse = resultObj; // raw response data is kept in a private variable
    
    var CartItem = function(itemData) {
        this.product = { 
            sku: {}
        };
        var prodRegEx = /^prod\.(.+)$/;
        var skuRegEx = /sku\.(.+)$/;
        var prodObj = { sku: {} };
        for (var prop in itemData) {
            var newPropName = null;
            var prodResult = prop.match(prodRegEx);
            if (prodResult && prodResult[1]) {
                newPropName = prodResult[1];
                this.product[newPropName] = itemData[prop];
            }
            if (!newPropName) {
                var skuResult = prop.match(skuRegEx);
                if (skuResult && skuResult[1]) {
                    newPropName = skuResult[1];
                    this.product.sku[newPropName] = itemData[prop];
                }
            }
            if (!newPropName) {
                this[prop] = itemData[prop];
            }
        }
    }

	var CartResult = function(responseData) {
	    var data = responseData;
	    var cartItem = {
	        product: { 
	            sku: {}
	        }
	    };
	    var cartMethod;
	    var allItems = [];

	    if (data.ac_results &&
	            Object.isArray(data.ac_results) && 
	            data.ac_results[0]) {
	        if (data.ac_results[0].result &&
	                data.ac_results[0].result.CARTITEM) {
	            cartItem = new CartItem(data.ac_results[0].result.CARTITEM);
	        }
	        if (data.ac_results[0].action) {
	            cartMethod = data.ac_results[0].action;    
	        }
	    }
	    
	    if (data.trans_data &&
	            data.trans_data.order &&
	            Object.isArray(data.trans_data.order.items) ) {
	        data.trans_data.order.items.each(function (itemData) {
	            var tempItem = new CartItem(itemData);
	            allItems.push(tempItem);
	        });
	    }
	    //------------------
	    // PUBLIC METHODS
	    //------------------
	    this.getAllItems = function() {
	        return allItems;
	    }
	    this.getItem = function() {
	        return cartItem;
	    };
	    this.getMethod = function() {
	        return cartMethod;
	    }
	};


    jsonRpcResponseObj.getId = function() {
        if (rawResponse) {
            return rawResponse.id;
        }
        return null;
    };
    jsonRpcResponseObj.getError = function() {
        if (rawResponse &&
            rawResponse.error) {
            return rawResponse.error;
        }
        return null;
    };
    jsonRpcResponseObj.getData = function() {
        if (rawResponse &&
            rawResponse.result &&
            rawResponse.result.data) {
            return rawResponse.result.data;
        }
        return null;
    };
    jsonRpcResponseObj.getValue = function() {
        if (rawResponse &&
            rawResponse.result &&
            typeof rawResponse.result.value != "undefined") {
            return rawResponse.result.value;
        }
        return null;
    };
    /**
     * This method returns the contents of the response's error property.
     * It first checks the result property, then checks the error property.
     */        
    jsonRpcResponseObj.getMessages = function() {
        if (rawResponse) {
            if (rawResponse.result &&
                rawResponse.result.data &&
                rawResponse.result.data.messages) {
                return rawResponse.result.data.messages;
            } else if (rawResponse.error &&
                       rawResponse.error.data &&
                       rawResponse.error.data.messages) {
                return rawResponse.error.data.messages;
            }
        }
        return null;
    };
    jsonRpcResponseObj.getCartResults = function() {
		var data = this.getData();
		if (!data) {
			return null;
		}
		var returnObj = new CartResult(data);
		return returnObj;	
    };

    return jsonRpcResponseObj;
};


generic.cookie = function(/*String*/name, /*String?*/value, /*.__cookieProps*/props){  
	var c = document.cookie; 
	if (arguments.length == 1) { 
		var matches = c.match(new RegExp("(?:^|; )" + name + "=([^;]*)"));
		if (matches) {
			matches = decodeURIComponent(matches[1]);
			try {
			     return matches.evalJSON(true); //Object
			} catch(e) {
			     return matches; //String
			}
			
		} else {
			return undefined;
		} 
	} else {
		props = props || {};
// FIXME: expires=0 seems to disappear right away, not on close? (FF3)  Change docs?
		var exp = props.expires;
		if (typeof exp == "number"){ 
			var d = new Date();
			d.setTime(d.getTime() + exp*24*60*60*1000);
			exp = props.expires = d;
		}
		if(exp && exp.toUTCString){ props.expires = exp.toUTCString(); } 
		
		value = encodeURIComponent(value);
		var updatedCookie = name + "=" + value;
		
		for(propName in props){
			updatedCookie += "; " + propName;
			var propValue = props[propName];
			if(propValue !== true){ updatedCookie += "=" + propValue; }
		}
		 
		document.cookie = updatedCookie;
	}
};
	  var generic = generic || {};
generic.flash = {
    abort: false,
    swfObject : swfobject,
    defaults : {
        defaultAlt : { 
            href: "http://www.adobe.com/go/getflashplayer"
        },
        attributes : { 
            playerversion: "9.0.28",
            width: "100%",
            height: "100%", 
                    hspace: 0,
                    vspace: 0,
                    align: "top"
        },
        params : {
            wmode: "transparent", 
                    quality: "high",
                menu: "true",
                    swliveconnect: "true",
                    allowscriptaccess: "always", 
                    scale: "noScale",
                    allowfullscreen: "true"
                } 
        },
    
    embed: function (attributes, params, placeholderId) { 
        if (generic.flash.abort) return;
     
        if (!$(placeholderId)) { 
            console.log("generic.flash.embed: Element doesnt exist"); 
            return; 
        }    
        
        /**
        //BUG: if two swfs are embedded at the same time (with no delay) & there is a "this" reference,
        //the second one is embedded twice.
                attributes = generic.mixin(generic.flash.defaults.attributes,attributes);
                params = generic.mixin(generic.flash.defaults.params,params);
                **/     
                   var defaults = {
        defaultAlt : { 
            href: "http://www.adobe.com/go/getflashplayer"
        },
        attributes : { 
            playerversion: "9.0.28",
            width: "100%",
            height: "100%", 
                    hspace: 0,
                    vspace: 0,
                    align: "top"
        },
        params : {
            wmode: "transparent", 
                    quality: "high",
                menu: "true",
                    swliveconnect: "true",
                    allowscriptaccess: "always", 
                    scale: "noScale",
                    allowfullscreen: "true"
                } 
                 };
                 
                attributes = Object.extend(defaults.attributes, attributes);
                params = Object.extend(defaults.params, params);
                 
            if (typeof params.flashvars != "string") { 
                params.flashvars = Object.toQueryString(params.flashvars); 
            }  
     
        // version check
        if (generic.flash.swfObject.hasFlashPlayerVersion(attributes.playerversion)) {   
            generic.flash.swfObject.addDomLoadEvent(function() { 
                generic.flash.swfObject.createSWF(attributes,params,placeholderId); 
            }); 
            return;
        }
         
        //content if flash doesn't embed
        var altid = attributes.altcontentid; 
        if (altid && $(altid)) {
            altid.style.visibility = "visible";
                    altid.style.display = "block";
        } else { 
            var defaultalt = $(placeholderId).select(".noflash")[0]; 
            if (!defaultalt) return; 
            if (!defaultalt.getAttribute("href")) {
                     defaultalt.observe("click", function() {
                window.open(defaults.defaultAlt.href);
               });
            }
            defaultalt.style.visibility = "visible";
                    defaultalt.style.display = "block";
          
        }     
    },

    /**
     * @namespace favorites contains favorites-related methods that are called by Action Script.
     * @memberOf generic.flash
     */
    favorites: {
        /* Used by Action Script to add items to a collection. Response data
         * is returned via a callback function.
         * @param args {object}
         * @param args.movieName {String} the value of the embed/object tag's name attribute
         * @param args.callback {String} the name that the container (the browser) uses to access the callback Function
         * @param args.skuBaseId {String} the value of the SKU_BASE_ID field for the SKU that is to be added
         * @methodOf generic.flash.favorites
         */
        add: function (args) {
            var options = Object.extend( {
                movieName : "",
                callback: "",
                skuBaseId: ""
            }, args);

            if ( options.skuBaseId.length < 1 ) {
                return null;
            }
            if ( !generic || !generic.checkout || !generic.checkout.cart ) {
                return null;
            }
            var cartObj = generic.checkout.cart;            
            var callbackFn = function(options, responseObj) {
                // console.log(responseObj.getMessages());
                if ( options.movieName.length > 1 &&
                    document[options.movieName] &&
                    document[options.movieName][options.callback] &&
                    typeof document[options.movieName][options.callback] === "function" ) {
                    if (responseObj.getMessages() || responseObj.getError()) {
                        document[options.movieName][options.callback](responseObj.getMessages());                        
                    }
                }
            }.curry(options);
            cartObj.updateCart({
                params: {
                    skus: [options.skuBaseId],           
                    itemType: "favorites",
                    action: "add"
                },
                onSuccess: callbackFn,
                onFailure: callbackFn
            });        
        }
    },

    /**
     * @namespace cart contains cart-related methods that are called by Action Script.
     * @memberOf generic.flash
     */
    cart: {
        /* Used by Action Script to add items to user's shopping cart. Response data
         * is returned via a callback function.
         * @param args {object}
         * @param args.movieName {String} the value of the embed/object tag's name attribute
         * @param args.callback {String} the name that the container (the browser) uses to access the callback Function
         * @param args.skus {Array} the value of the SKU_BASE_ID field for each SKU that is to be added
         * @param args.quantity: {Number} the quantity of items that will be added to the cart.             
         * @methodOf generic.flash.cart
         */
        add: function (args) {
            var options = Object.extend( {
                movieName : "",
                callback: "",
                skus: [],
                quantity: 1
            }, args);
            if ( options.skus.length < 1 ) {
                return null;
            }
            if ( !generic.checkout || !generic.checkout.cart ) {
                return null;
            }
            var cartObj = generic.checkout.cart;            
            var callbackFn = function(options, responseObj) {
                if ( options.movieName.length > 1 &&
                        document[options.movieName] &&
                        document[options.movieName][options.callback] &&
                        typeof document[options.movieName][options.callback] === "function" ) {
                    if (responseObj.getData()) {
                        document[options.movieName][options.callback](responseObj.getData().ac_results);                        
                    } else if (responseObj.getError()) {
                        document[options.movieName][options.callback](responseObj.getMessages());
                    }
                }
            }.curry(options);
            cartObj.updateCart({
                params: {
                    skus: options.skus,            
                    INCREMENT: 1
                },
                onSuccess: callbackFn,
                onFailure: callbackFn
            });         
        }
    }
};generic.checkout = {};

/**
 * generic.checkout.cart 
 * - depends on: generic.cookie, generic.jsonrpc
 */ 
generic.checkout.cart = ( function() {

    return {
        setCookie: false, 
    
        order: new Hash(),
        payments: new Array(),
        carts: new Hash(), 
        items: new Array(),
        samples: new Array(),
     
        totalShoppedItems: 0, 
        totalItems: 0,
     
        transactionParams: {
        transactionFields: {
                    "trans_fields" : ["TRANS_ID", "payments"]
            },
        paymentFields: {
                    "payment_fields" : ["address", "PAYMENT_TYPE", "PAYMENT_AMOUNT", "TRANS_PAYMENT_ID"]
            },
        orderFields: {
                    "order_fields" : ["items", "samples", "address", "TRANS_ORDER_ID"]
            }
        },
    
        itemTypes: { 
            "cart" : {
                "id": "SKU_BASE_ID",
                "_SUBMIT" : "cart"
            },
            "giftcard" : {
                "id": "CART_GIFTCARD_ID",
                "_SUBMIT" : "giftcard"
            },
            "collection" : {
                "id": "SKU_BASE_ID",
                "_SUBMIT" : "collection.items"
            },
            "favorites" : {  
                "id": "SKU_BASE_ID",
                "_SUBMIT" : "alter_collection"
            }
        },

        initialize: function(args) { 
            Object.extend(this, args); // copy args to obj
        },
    
        getCartTotals: function() {
            var cookie = generic.cookie("cart");
            if (cookie && cookie!==null) {
               // console.log("generic.cart.getCartTotals cookie: "+Object.toJSON(cookie)); 
               Object.extend(this, cookie);
           
               generic.events.fire({event:'cart:countsUpdated'});
            } else {
               // console.log("generic.cart.getCartTotals !cookie");  
               this.getCart();
            }  
        },

        // MERGE NOTE: MAC-specific
        //update Cookie, in case Cart has been updated via form submission instead of generic.cart 
        updateCartTotals: function(args) { 
             //console.log("generic.cart.updateCartTotals: "+Object.toJSON(args));  
             Object.extend(this, [args]);
             if (this.ifSetCookie) this.setCookie();    
             generic.events.fire({event:'cart:countsUpdated'});
        },
    
        setCookie: function() { 
            console.log("generic.cart.setCookie "+this.totalItems);
            var s  = {
                totalItems: this.totalItems 
            }
            s = Object.toJSON(s);
            generic.cookie("cart",s, {path:"/"}); 
        },
    
        getCart: function(args) {
            //console.log("generic.cart.getCart");
            var self = this; 

            if (args != null && args.pageDataKey) {
                var pageData = generic.page_data(args.pageDataKey);
                if (pageData.get("rpcdata")) {
                    // console.log( "cart page data found!" );
                    self._updateCartData(pageData.get("rpcdata"));
                    return;
                }
            }

            var params = {};
            params = Object.extend ( params, self.transactionParams.transactionFields );
            params = Object.extend ( params, self.transactionParams.paymentFields);
            params = Object.extend ( params, self.transactionParams.orderFields);
        
             var id = generic.jsonrpc.fetch({
                method : 'trans.get',
                params: [params],
                onSuccess:function(jsonRpcResponse) {
                    self._updateCartData(jsonRpcResponse.getValue()); 
                },
                onFailure: function(jsonRpcResponse){
                    //jsonRpcResponse.getError();
                    console.log('Transaction JSON failed to load');
                }
            });
            return id;
        },
   
         //sets up internal representation of cart state
        // Assuming one order per transaction
        _updateCartData: function(data){ 
            // console.log("generic.checkout.cart._updateCartData");
            var self = this;
            this.data = data;
            if (!data.order) return; // MAC: trans_data can exist for transactions where trans_data.order does not, such as favorites where free shipping message is returned in trans_data
            this.totalItems = data.items_count; 
            this.defaultCartId = data.default_cart_id;
            this.payments = (data.trans && data.trans.payments) ? $A(data.trans.payments) : null;  
            this.order = data.order; 
         
            // contents and sample_contents mirror the sku by qty hashes
            this.order.contents = new Hash();
            this.order.sample_contents = new Hash();

            if (this.order.items != null) {
                this.order.items = this.order.items.reject(function(item){ // filter out nulls
                    return item === null;
                });
            }
        
            var items = this.order.items || null;
            var totalShoppedItems = 0;
            if (items != null) {  
                items.each(function(item){   
                    if (!item) { return; }
                    totalShoppedItems+=item.ITEM_QUANTITY;
                
                    // set up contents by cart hashes 
                    var cartID = item.CART_ID;
                    var cart = self.carts.get(cartID);
                    if (!cart) {
                        self.carts.set(cartID, new Hash()); 
                        cart = self.carts.get(cartID); 
                        cart.set('contents', new Hash());
                    } 
                    var id = item['sku.SKU_BASE_ID'] ? item['sku.SKU_BASE_ID'] : item.COLLECTION_ID; 
                    cart.get('contents').set(id, item.ITEM_QUANTITY);
                
                    // compute per-unit tax (replace this with field from JSONRPC result when available)
                    var unitTax = item.APPLIED_TAX/item.ITEM_QUANTITY;
                    item.UNIT_TAX = unitTax;

                    // set up order contents hash (spans carts)
                    if (item.itemType.toLowerCase() == 'skuitem') {
                        var key = item['sku.SKU_BASE_ID'];
                        var qty = item.ITEM_QUANTITY; 
                        //error self.order.contents.set(key, qty);
                        self.order.contents[key] = qty; 
                    } else if (item.itemType.toLowerCase() == 'kititem') {
                        var key = item.COLLECTION_ID;
                        var qty = item.ITEM_QUANTITY;
                        self.order.contents.set(key,qty);
                    } else {
                        // FUTURE: other cart item types (e.g. kits)
                    } 
                
                });
            }
        
            this.totalShoppedItems = totalShoppedItems;
        
            var samples = this.order.samples;
            if (samples != null) {
                samples.each(function(item){
                    // set up contents by cart hashes
                    var cartID = item.CART_ID;
                    var cart = self.carts.get(cartID);
                 
                    if (!cart) {
                        self.carts.set(cartID, new Hash());
                        cart = self.carts.get(cartID);
                        cart.set('contents', new Hash());
                    } 
               
                    var id = item['sku.SKU_BASE_ID'] ? item['sku.SKU_BASE_ID'] : item.COLLECTION_ID; 
                    cart.get('contents').set(id, item.ITEM_QUANTITY);

                    // set up order contents hash (spans carts)
                    if (item.itemType.toLowerCase() == 'sampleitem') {
                        var key = item['sku.SKU_BASE_ID'];
                        var qty = item.ITEM_QUANTITY;
                        self.order.sample_contents.set(key,qty); 
                    } else {
                        // other item types (are likely errors)
                    } 
                });
            }
        
            // if (self.setCookie) self.setCookie();        
            generic.events.fire({event:'cart:countsUpdated'});
            // generic.events.fire({event:'cart:updated'}); 
        },
   
        /* args is a hash
        required keys: args.params.skus array, either args.params.INCREMENT or args.QTY with args.INCREMENT overriding args.QTY
        optional keys: args.itemType, args.OFFER_CODE, args.CART_ID*/ 
        updateCart: function(args){
            //console.log("cart.updateCart: "+Object.toJSON(args.params));
        
            if (!args.params) return null; 
            var self = this;   
            var onSuccess = args.onSuccess || Prototype.emptyFunction;  
            var onFailure = args.onFailure || Prototype.emptyFunction; 
            var itemType = args.params.itemType || "cart"; //e.g. cart, collection, giftcard etc
            var id = self.itemTypes[itemType].id;
            var qty = args.params.QTY;
            var action = args.params.action;
            var increment = args.params.INCREMENT;
            var skus = args.params.skus;
         
            var params = {
                '_SUBMIT': self.itemTypes[itemType]["_SUBMIT"] 
            };  

            if (typeof args.params.CAT_BASE_ID != "undefined"){ 
                params["CAT_BASE_ID"] = args.params.CAT_BASE_ID; 
            }else{
                console.log("MISSING CAT_BASE_ID");
            }
            //console.log("cat base id in updateCart ", params['CAT_BASE_ID']); 

            //id 
            params[id] = (skus.length == 1) ? skus[0] : skus; //MK collections array syntax correct?
            //qty   
            if (increment && increment>=0) {
               //currently +1 will be added regardless of INCREMENT's actual value
               //backend requires QTY property to exist but it will not be used
             params["INCREMENT"] = increment; 
             params["QTY"] = 1; 
            } else if (increment && increment<0) {
                //decrements qty by -1
            } else if (typeof(qty) !== "undefined" && qty>=0) { 
                params["QTY"] = qty;  
            }
                     
            //offer code
            if (args.params.OFFER_CODE && args.params.OFFER_CODE.length>0) {
                params['OFFER_CODE'] = args.params.OFFER_CODE;
            }
            
            // SS: for some reason this was set to always be "add"
            // need "delete" option for removing favorites
            if (action && (action.length > 0)) {
                params['action'] = action;
            }

            // targeting of the correct cart is still missing (and important to get right) 
            // cart id if we are adding to something other than the default cart
            if (args.params.cart_id && (args.params.cart_id != self.defaultCartId)) {
                params['CART_ID'] = args.params.cart_id;
            }
       
            var id = generic.jsonrpc.fetch({
                "method" : 'rpc.form',
                "params" : [params],
                "onSuccess": function(jsonRpcResponse){
                    var data = jsonRpcResponse.getData();
                    //load data
                    if (data && data["trans_data"]) { 
                        self._updateCartData(data["trans_data"]);
                    }
                    if (itemType === 'cart') {
                        var cartResultObj = jsonRpcResponse.getCartResults();
                        document.fire("cart:updated", cartResultObj);
                    };
                    if (itemType == 'favorites') {
                        document.fire("favorites:updated", jsonRpcResponse);
                    };
                    onSuccess(jsonRpcResponse);
                },
                "onFailure": function(jsonRpcResponse){
                    onFailure(jsonRpcResponse);
                }
            });
 
            return id;
        }, 
    
        getItemQty : function(baseSkuId) {  
            if (!this.order.items) return 0;
            var lineItem = this.order.items.find( function (line) {
                return line['sku.SKU_BASE_ID'] ==  baseSkuId;
              });  
            if (!lineItem) {
                return 0; 
            }                
            return lineItem.ITEM_QUANTITY;
        },
    
        getBaseSkuIds: function() {  //MK: what is this used for?
            //console.log("generic.cart.getBaseSkuIds: "+this.order.items);
            if (!this.order.items) return new Hash();
            var baseSkuIds = this.order.items.pluck( 'sku.SKU_BASE_ID' ); //MK what about giftcards/collections?  
            return baseSkuIds; 
        },  
    
        getSubtotal: function() {
            var lineItems = this.order.items;
            if (!this.order.items) return 0;
            var subtotal = 0;
            for (var i=0, len = lineItems.length; i<len; i++) {
                var lineItem = lineItems[i];
                subtotal += (lineItem.UNIT_PRICE + lineItem.UNIT_TAX) * lineItem.ITEM_QUANTITY;
            }
            return subtotal;
        },
     
        getTotalShoppedItems: function(){ //products and gift cards
           /** var ttl = 0;
            var items = this.order.items;
            if (items != null) {
                items.each(function(item){
                    if (item && item.ITEM_QUANTITY) {
                        ttl += item.ITEM_QUANTITY;
                    }
                });
            } 
            return ttl;**/
            return this.totalShoppedItems;
        },         
        
        getTotalSamples: function() {
             var ttl = 0;
             var samples = this.order.samples;
                if (samples != null) {
                    samples.each(function(item){
                        ttl += item.ITEM_QUANTITY;
                    });
            } 
            return ttl;
        }, 
    
        getTotalItems: function(){ 
           // return this.getTotalShoppedItems() + this.getTotalSamples();
           return this.totalItems;
         }    
     };
}() ) ;

generic.popup = function(/*Object*/args) {
    var activatorNode = $(args.activator);
    if (!activatorNode) { return false; }
        
    //console.log("creating popup for "+activatorNode); 
    var specs = Object.toQueryString(Object.extend(generic.popup.defaults, args));
    var specs = specs.replace(/\&/g, ",");
     
    var open = function(clickEvent) {
        clickEvent.preventDefault();
        var win = window.open(args.url, args.name, specs);
        if (!win) generic.popup.errorAction(); 
    };

    activatorNode.observe("click", open); 
    
    return true;
}

generic.popup = Object.extend(generic.popup, {
    defaults: {  
        height: 500,
        width: 500,
        top: 25,
        left: 25,
        resizable: "yes",
        scrollbars: "yes",
        status: "no",
        toolbar: "no",
        menubar: "no",
        location: "no"
    },
    errorAction: function() {
         var msg = global.rb.popup_error_message;
         if (msg) alert(msg);
    }
});var extendElement = {
     /* gets element's ancestor with specified tagname/classname **/
     getAncestor: function(element, s){
       	var ancestors = element.ancestors();
		var type = "tagName";
		if (s.substring(0,1)==".") {
			type = "className";
			s = s.substring(1,s.length);
		} 
		var oRegExp = new RegExp("(\\b)" + s + "(\\b)");
		var result = false;
		find(result); 
		return result;
		
		function find(o) { 
			for (var i=0; i<ancestors.length; i++) { 
				if (oRegExp.test((type=="tagName")? ancestors[i].tagName : ancestors[i].className)) {  
					result = ancestors[i]; 
					break;
				}  
			}  
		}
    },
    
    /* gets element's direct children with specified tagname/classname **/	
    getChildren: function(element, s) {
		var children = element.childElements();
		var type = (s.substring(0,1)==".") ? "className" : "tagName";
		s = s.replace(/\./,"");
		var oRegExp = new RegExp("(\\b)" + s + "(\\b)");
		var results = [];
	
		children.each(function($_) {
        	if (oRegExp.test((type=="tagName")? $_.tagName : $_.className)) results.push($_);
       	}); 
		 
		return results;
	}
}
Element.addMethods(extendElement);

 
	 
/**
 * generic.user
 * - depends on: generic.jsonrpc
 */
generic.user = (function(){

    return {
        signed_id : false,

        timeoutLength : 15 * 60 * 1000,

        initialize: function(args) {
            generic.updateProperties.apply(this, [args]);
        },

        getUser: function(args) {
            var self = this;

            if (args != null && args.pageDataKey) {
                var pageData = generic.page_data(args.pageDataKey);
                if (pageData.get("rpcdata")) {
                    console.log( "user page data found!");
                    self._updateUserData(pageData.get("rpcdata"));
                    return;
                }
            }

            var id = generic.jsonrpc.fetch({
                method : 'user.json',
                params: [],
                onSuccess: function(jsonRpcResponse) {
                    self._updateUserData(jsonRpcResponse.getValue());
                },
                onFailure: function(jsonRpcResponse) {
                    console.log('User JSON failed to load');
                }
            });
            return id;
        },

        // until we better parameterise this...
        _updateUserData: function(data) {
            var seld = this;
            if (data != null && data[this.userinfo_rpc_key] != null) {
                Object.extend( this, data[this.userinfo_rpc_key] );
            } else {
                Object.extend( this, data );
            }
            generic.events.fire({event:'user:updated'});
        },

        isSignedIn: function() {
            return ( this.signed_in ? true : false );
        }

    };
}() );

// log out user after 15 minutes on secure pages
if (document.location.protocol == 'https:') {
    var logout = function() {
//        alert(generic.user.timeoutLength);
        document.location.href = '/account/signin.tmpl?timeout=1';
    };
    window.setTimeout( logout, generic.user.timeoutLength );
}
var generic = generic || {};

generic.errorStateClassName = 'error';

/**
 * This method displays error messages. It takes an array of error objects and a UL node
 * as parameters. If the UL is not spuulied, it will attempt to find a <UL class="error_messages">
 * in the DOM. It will then attempt to insert one directly after a <DIV id="header"> (If no header
 * is found, the method exits.) All the LI child nodes (previous messages) of the UL are hidden.
 * The text property of each error Hash is then displayed as an LI.
 * This method can also alter the style of the input elements that triggered the error.
 * The tags property in an error hash must be an array that contains a string starting with
 * "field." If the optional formNode parameter is supplied, this form node will be
 * searched for the field, and that field will be passed to the generic.showErrorState method. 
 * @example 
 * var errArray = [
 *      {
 *          "text": "Last Name Alternate must use Kana characters.",
 *          "display_locations": [],
 *          "severity": "MESSAGE",
 *          "tags": ["FORM", "address", "field.last_name_alternate"],
 *          "key": "unicode_script.last_name_alternate.address"
 *      },
 *      {
 *          "text": "First Name Alternate must use Kana characters.",
 *          "display_locations": [],
 *          "severity": "MESSAGE",
 *          "tags": ["FORM", "address", "field.first_name_alternate"],
 *          "key": "unicode_script.first_name_alternate.address"
 *      }
 *  ];
 * var listNode = $$("ul.error_messages")[0];
 * generic.showErrors(errArray, listNode);
 * @param {Array} errorObjectsArray Array of error hashes.
 * @param {DOM Node UL} errListNode UL element in which the error messages will be displayed.
 * @param {DOM Node} formNode Form element (or any container node) that contains the inputs
 * to be marked as being in an error state. 
 */
generic.showErrors = function(errorObjectsArray, errListNode, formNode) {
    var ulNode = errListNode || $$("ul.error_messages")[0];
    if (!ulNode) {
        ulNode = new Element("ul", {"class":"error_messages"});
        var header = $$("div#header")[0];
        if (!header) {
            return null;
        } else {
            header.insert({after:ulNode});
        }
    }
    var errListItemNodes = ulNode.select("li");
    errListItemNodes.each(function(li){
        li.hide();
    });
    ulNode.addClassName("errors-no-messages");
    if (errorObjectsArray.length > 0 && Object.isElement(formNode)){
        // hide all error states on fields
        var inputNodes = formNode.select("input");
        inputNodes = inputNodes.concat(formNode.select("select"));
        inputNodes = inputNodes.concat(formNode.select("label"));
        inputNodes.each(function(inputNode) {
            generic.hideErrorState(inputNode);
        });
    }
    errorObjectsArray.each(function(errObj){
        var errKey = errObj.key;
        var errListItemNode = null;
        if (errKey) {
            var regEx = new RegExp(errKey);
            // try to find LI whose ID matches the error key 
            errListItemNode = errListItemNodes.find(function(node) {
                return regEx.test(node.id);
            });
        }
        if (errListItemNode) {
            errListItemNode.show();
        } else {
            errListItemNode = new Element("li").insert(errObj.text);
            ulNode.insert(errListItemNode);
        }
        if (errObj.displayMode && errObj.displayMode === "message") {
            errListItemNode.addClassName("message");
        }
        if (errObj.tags && Object.isArray(errObj.tags) && formNode) {
            // search through error objects, show error state for any tagged with "field.[NAME]"
            var fieldPrefixRegEx = /^field\.(\w+)$/;
            errObj.tags.each( function(tag) {
                var reResults = tag.match(fieldPrefixRegEx);
                if(reResults && reResults[1]) {
                    var fieldName = reResults[1].toUpperCase();
                    var inputNode = formNode.select("input[name=" + fieldName + "]")[0] || formNode.select("select[name=" + fieldName + "]")[0];
                    if (inputNode) {
                        generic.showErrorState(inputNode);
                        var labelNode = formNode.select("label[for=" + inputNode.id + "]")[0];
                        generic.showErrorState(labelNode);                      
                    }
                }
            });
        }

    });
    ulNode.show();
    if (errorObjectsArray.length > 0){
        ulNode.removeClassName("errors-no-messages");
    }
};
generic.showErrorState = function(inputNode) {
    if (!inputNode || !Object.isElement(inputNode)) {
        return null;
    }
    inputNode.addClassName(generic.errorStateClassName);
}

generic.hideErrorState = function(inputNode) {
    if (!inputNode || !Object.isElement(inputNode)) {
        return null;
    }
    inputNode.removeClassName(generic.errorStateClassName); 
}
var generic = generic || {};
generic.page_data = generic.page_data || {};
var page_data = page_data || {};

generic.page_data = function(pageDataKey) {
    var getPageDataValue = function(pageDataKey) {
        if (!Object.isString(pageDataKey)) {
            return $H({});
        }
        // avoid deep cloning of page_data
        if (pageDataKey && page_data) {
            var key = pageDataKey;
            var parts = key.split(".");
            var length = parts.length;
            var val = page_data;
            var k;
            while (k = parts.shift()) {
                if (val[k]) {
                    val = val[k];
                }
                else {
                    return $H({});
                }
            }
            var rh;
            // For scalars and arrays make a return hash where the key is the pageDataKey
            if (Object.isString(val) || Object.isNumber(val) || Object.isArray(val)) {
                var t = new Object;
                t[pageDataKey] = val;
                rh = $H(t);
            }
            else {
                rh = $H(val);
            }

            if (rh) {
                return rh;
            }
            else {
                return $H({});
            }
        }
        else {
            return $H({});
        }
    };
    var pageDataValue = getPageDataValue(pageDataKey);
    var returnObj = {
        get: function(key) {
            if (!Object.isString(key)) {
                return null;
            }
            var val = pageDataValue.get(key);
            return val;
        }
    };
    return returnObj;
};

var brand = {};

/**
 * Brand extensions of generic.flash
 */
generic.flash.playerversion = (generic.env.isMac) ? "10.0.0" : generic.flash.playerversion;
// generic.flash.cart.add Brand-specific feature(s): 
// - Send result.data.trans_data & result.data.ac_results back to flash (vs. just ac_results)
generic.flash.cart.add = function (args) {
    var options = Object.extend( {
        movieName : "",
        callback: "",
        skus: [],
        quantity: 1
    }, args);
    if ( options.skus.length < 1 ) {
        return null;
    }
    if ( !generic.checkout || !generic.checkout.cart ) {
        return null;
    }
    //console.log("args passed from flash: "+Object.toJSON(options));
    var cartObj = generic.checkout.cart;
    var catId = options.catId;
    if (catId) {
        var match = catId.split("CAT");
        catId = (match ? match[1] : catId);
    }
    var callbackFn = function(options, responseObj) {
        if ( options.movieName && (options.movieName.length > 1) && $(options.movieName) ) {
            var flashObject = $(options.movieName);
            var externalCallback = options.callback;
            if (externalCallback && typeof flashObject[externalCallback] === "function" ) {
                // send result.data.trans_data & result.data.ac_results
                var responseData = responseObj.getData();
                if (responseData) {
                    flashObject[externalCallback]({trans_data: responseData.trans_data, ac_results: responseData.ac_results});
                } else if (responseObj.getError()) {
                    //console.log("generic.flash.cart.add callback: sending error/messages "+Object.toJSON(responseObj.getMessages()) );
                    externalCallback(responseObj.getMessages());
                }
            } else {
                console.log("generic.flash.cart.add callback: failure on callback argument = ",flashObject[externalCallback]);
            }
        }
    }.curry(options);
    cartObj.updateCart({
        params: {
            skus: options.skus,            
            INCREMENT: 1,
            CAT_BASE_ID: catId
        },
        onSuccess: callbackFn,
        onFailure: callbackFn
    });         
};
        

/**
 * Takes a hex value and returns equivalent rgb value
 * @param {String} hex  
 * @memberOf brand
 * @example
    generic.hexToRGB("#FFFDF4");
 */
brand.hexToRGB = function(hex) {
    var rgb = [];
    if(!hex) return [0, 0, 0];
    var h = cutHex(hex);
    rgb.push( parseInt( h.substring(0,2),16) );
    rgb.push( parseInt( h.substring(2,4),16) );
    rgb.push( parseInt( h.substring(4,6),16) ); 
    //console.log("hexToRGB: "+hex+" = "+rgb[0]+" "+rgb[1]+" "+rgb[2]);
    return rgb;
    
    function cutHex(h) {return (h.charAt(0)=="#") ? h.substring(1,7):h} 
}

/**
 * brand.updateProperties.apply
 * Needed for cases where Object.extend doesn't return what's expected
 * @memberOf brand
 * @example
    brand.updateProperties.apply(this, [args]);
 */
brand.updateProperties = function(obj) {   
    if (!obj) return;
    for (prop in obj) {  
        this[prop] = obj[prop];
    }  
}


/**
 * brand.tabs
 * Tabbed Container
 * Use this class IF you have created the html manually
 * and you just want to wrap it with the class object.
 * @memberOf brand
 */
brand.tabs = Class.create(Control.Tabs,
{
    // default Control.Tab options
    options: {
        activeClassName: 'tab-active',
        setClassOnContainer: true
    },
       
    initialize: function($super, containerId, args) {
        var options = this.options;
        Object.extend(options, args || {});
        Object.extend(this, args || {});

        if (args.scrollbar) this.initScrolling();        
        $super(containerId, options);
        if (args.useImageHeaders) this.initHeaders();
    },
  
    setActiveTab: function($super, link) {
        if (link.id === this.activeLink.id) return;
        if (this.beforeShow) this.beforeShow(link);
        if (this.scrollbar && this.tabContainer) {
            this.tabContainer.removeClassName(this.scrollbar.enabledClass); // start w/ scrollbar hidden
        }
        if (this.imgHeaders) {
            var linkImg = this.imgHeaders[link.id];
            if (linkImg) linkImg.changeSrc("on");
            var activeLinkImg = this.imgHeaders[this.activeLink.id];
            if (activeLinkImg) activeLinkImg.changeSrc("off");
        }
        
        $super(link);
        this.resetScrolling();
    },

    onContentRefresh: function() {
        this.resetScrolling();
    },

    initHeaders: function() {
        var imgs = {}
        this.links.each(function(link) {
            var imgNode = link.select('img')[0];
            if (imgNode) imgs[link.id] = new brand.img(imgNode, ["on", "off"]);
        });
        this.imgHeaders = imgs;
    },
    
    initScrolling: function() {
        var contentNode = this.scrollbar.contentNode;
        var handleId = this.scrollbar.handleId;
        var trackId = this.scrollbar.trackId;
        if (!contentNode || !handleId || !trackId) return;
        if (!$(handleId) || !$(trackId)) return;
        
        // scroll the element vertically based on its width and the slider maximum value
        var scroll= function(value) {
            contentNode.scrollTop = Math.round(value / scrollbar.maximum * (contentNode.scrollHeight - contentNode.offsetHeight) );
        }
        
        var scrollbar = new Control.Slider(handleId, trackId, {
            axis: 'vertical',
            onSlide: scroll,
            onChange: scroll
        });
        
        this.scrollbarObj = scrollbar;
        this.scrollbarNode = this.scrollbar.containerNode;
    },
    
    resetScrolling: function() {
        var scrollbarObj = this.scrollbarObj;
        if (!this.scrollbar || !scrollbarObj) return;
        var contentNode = this.scrollbar.contentNode;
        var scrollbarNode = this.scrollbar.containerNode;
        // disable/enable scrolling depending on overflow/height of scrollable content
        scrollbarObj.setValue(0);
        if (contentNode.scrollHeight <= contentNode.offsetHeight) {
            scrollbarNode.hide();
            //scrollbarObj.setDisabled();
        } else {
            //scrollbarObj.setEnabled();
            scrollbarNode.show();
            //this.tabContainer.addClassName(this.scrollbar.enabledClass);
        }
    },
    
    updateTab: function(tabId, html) {
        var containerNode = $(tabId);
        if ( containerNode ) {
            containerNode.update ( html );
        }
    }

});

/**
 * brand.bottomFixed
 * simulate fixed bottom position
 * @memberOf brand
 */ 
brand.bottomFixed = Class.create({  
    node: null,  // node: DOM element to position 
    minTop: 0, // minTop: Number minimum pixels for node.style.top 
    isLoaded: false,

    initialize: function(args) {
        if (!args.node) return false;
        this.node = args.node;
        this.s = this.node.style;
        var observeResize = (args.observeResize == false ? false : true);
        if (args.bottom) {
            this.fromBottom = args.bottom;
        } else {
            this.fromBottom = parseInt(this.node.getStyle("bottom"), 10);
        }
        if (isNaN(this.fromBottom)) { 
            console.log("brand.bottomFixed: bottom is NaN");
            return;
        }        
        if (args.minTop) {
            this.minTop = args.minTop;
            this.hasMinTop = true;
        } else {
            this.hasMinTop = false;
        }
        
        // initial top position
        if (args.startingTopPosition == 0 || args.startingTopPosition) {
            this.s.top = args.startingTopPosition + "px";
        } else {
            this.position();
            this.s.bottom = '';
            this.s.visibility = "visible";
        }
        
        // set scroll events
        var self = this;
        Event.observe(window, 'scroll', function() {
            self.onScroll();
        });
        if (observeResize) {
            Event.observe(window, 'resize', function() {
                self.onScroll();
            });
        }

        this.isLoaded = true;
    },
    position: function() {  
        var h = window.pageYOffset ||
        document.documentElement.scrollTop;
        h = (h ? h : 0);
        var shiftY = ((h + document.documentElement.clientHeight) - (this.node.offsetHeight + this.fromBottom));
        if (isNaN(shiftY)) return;
        if (this.currentY != shiftY) {  
            var changeBy = shiftY;      
            if (this.hasMinTop && (changeBy <= this.minTop)) {
                changeBy = this.minTop;
            }
            this.currentY = changeBy;
            this.s.top = (changeBy + 'px');  
        }   
    },
    onScroll: function() {   
        this.position();
    }
});var site = brand;

site.init = function() {
    if (document.URL.indexOf("?fb_xd_fragment#") > -1) return; // fix for IE6 & Facebook library to avoid double-loading of brand code
    /** testing **/
    //generic.flash.abort = true;  
    //site.view.colorNav.abort = true; 
    //site.globalnav.abort = true;
    //site.checkout.abort = true;  
    //site.product.abort = true;
    //site.customerService.abort = true;
       
    /** temporary changes **/
    //turn off features not ready for launch

    generic.init(); 
    generic.templatefactory.templateHash = "jsTemplates";

    // brand extention(s) of generic
    generic.overlay = brand.overlay;

    getGlobalRBKeys();
    site.setGlobalParams();
    
    // locale/site extensions:
    // Add file suffix property to all templated widget instances via master Widget class
    Widget.addMethods({ imgSuffix: global.localeFileSuffix });
    
    site.forms.init();
    
    try {
        var id = page_data.panel_nav["default"].id;
    } catch(e) {
        console.log("page_data.panel_nav missing");
        return;
    }
    
    site.globalnav.init();
    site.view.init();
    site.initLocaleHandling();
    
    site.account.init();
    site.checkout.init();
    site.product.init();
    site.customerService.init();    
    site.givingBack.init();
    if (!global.ispro) site.livechat.init();
    
    site.overlay.initLinks();
    
    //try { // temporary for testing
        //site.coremetrics.liveperson.track(); // to be replaced with new code
    //}
    //catch(err) {
        //console.log("site.coremetrics.liveperson err = "+err);
    //}
};

document.observe("dom:loaded", site.init );


//---------------------------------------------global settings
var global = global || {};
global.locale = "en_us";
global.ispro = false;
global.isprostudent = false;
global.isipad = false;

site.setGlobalParams = function () {
    var proFileSuffix = "";
    if (page_data) {
        if (page_data.customer && page_data.customer.is_pro === 1) {
            global.ispro = true;
            proFileSuffix = "_pro";
            if (page_data.customer.is_pro_student === 1) global.isprostudent = true;
        }
        if (page_data.is_ipad_user_agent && page_data.is_ipad_user_agent == 1) {
            global.isipad = true;
        }
    }
    global.localeFileSuffix = ""; // default
    global.localeFileSuffixes = { // exceptions where needed
        "flash_home" : proFileSuffix
    }
};

// start up methods for language toggling and locale-specific behavior
site.initLocaleHandling = function() {
    site.view.utilityNav.initLanguageToggle({ locale: global.locale });
    MP.initRedirectLinks(); // US only
}

//-----------------------------------------------------rb keys
var getGlobalRBKeys = function () {    
    generic.rb.language = generic.rb("language");
    global.rb = global.rb || {};
    global.rb.popup_error_message = generic.rb.language.get("popup_error_message");
    generic.rb.language.rb_close = generic.rb.language.get('close');
    //global.rb.download_flash = generic.rb.language.get("download_flash"); // needed?
};


//-----------------------------------------------------hacks
//cms-output html panel_navs sometimes have this ancient js
//eg. Artists panel or Customer Service panel
var el = function() { return legacy};
var legacy = { addBehavior:function() {}} 
var BehaviorRollover = false;  


//-----------------------------------------------------legacy    
generic.flash.Api = {};
generic.flash.Api.jsCall = function(method, args) { 
    if (generic.flash.ApiMethods[method]) {
        var resp = generic.flash.ApiMethods[method](args);
        return resp;
    } else {}  
};

generic.flash.ApiMethods = { 
    cuePoint: function() {  
        var args = arguments[0];
        generic.events.fire({event:"videoPlayer:cuePoint", msg:args}); 
    //  var inc = Object.toJson(args);
    //  return {"results": args} 
    },

    cuePointProduct: function(args) {
        var passthru = args[0].actions[0];
        this.cuePoint(passthru);
    },

    alterCart: function(args) {   
        console.log("generic.flash.apiMethods.alterCart "+Object.toJSON(args));
        site.checkout.alterCart.alter(args); 
    },

    pageData: function(nargs) {
        console.log("generic.flash.ApiMethods.pageData");
        var args = nargs[0];

        // custom palette 
        if (typeof args === "undefined") {                                                                 
            try {                                                                                      
                if (nargs["query"] && nargs["query"] === "palette" && site.product.customPalette) {                                     
                    site.product.customPalette.pageData(nargs);
                    return;
                }                                                        
            } catch(e) {}                                                                              
        }
         
        var result;
        var pd = parent.page_data;
        if (!pd) return;
        if (args && args.query) {
            //console.log("retrieving page_data with query: " + args.query);
            var path = args.query.split(".");
            var length = path.length;
            var value = pd;
            for (var i = 0; i < length; i++) {
                var key = path.shift();
                value = value[key];
            }
            result = value;
        } else {
            result = pd;
        }
        return { "results": result } 
    }, 
    
    notifyEvent: function(nargs) {    
        site.view.colorNav.setWidth(nargs.event);
    },
    
    setElementSize: function(nargs) {  
        //brand.view.setElementSize(nargs[0]);
        site.view.productBrowser.resizeEmbedContainer(nargs[0]);
    }
};


// cms inline functions
//-----------------------------------------------------

function openFullWindow(url, name, w, h) {
    var w = window.open(url, name, "menubar=1, toolbar=1, resizable=1, scrollbars=1, width=" + w + ", height=" + h);
};

function openLiveChatPopup() {
    if (!site.livechat) return;
    site.livechat.openLiveChatPopup();
};

/**
 * brand.menu
 * Class: Creates a menu whose display is triggerd by mouse events over targetId
 * @memberOf brand
 */
brand.menu = Class.create( {

    // targetId: String
    // id of dom node that triggers display of menu
    targetId: "",
    
    // menuId: String
    // id of menu dom node
    menuId: "",
    
    timer: null,    
    timerDuration: 3,

    /**
     * brand.menu.initialize
     * Method for creating an instance of brand.menu
     * @param {String} args.menu  Menu node Id
     * @param {String} args.target  Id of node which triggers display of menu 
     * @example
        var footerMenu = new brand.menu({
            menu: "countries_container",
            target: "countries_hd"
        });
     * @methodOf brand.menu
     */
    initialize: function(/* Object */args) { 
        this.menuId = args.menu;
        var target = $(args.target);
        var menu = $(this.menuId);
        if (menu && target) {
        this.handlers = [
            target.observe('mouseover', this.show.bind(this)),
            target.observe('mouseout', this.startHide.bind(this)),
            menu.observe('mouseover', this.keepMenu.bind(this)),
            menu.observe('mouseout', this.startHide.bind(this))
        ];
    }
    },
    show: function(e) {
            //console.log("calling  show from "+e.target);
        this.keepMenu(e);
        $(this.menuId).removeClassName("hidden");
    },
    startHide: function(e) {
            //console.log("calling startHide from "+e.target); 
        this.timer = setTimeout(this.hide.bind(this), this.timerDuration);
        Event.stop(e);
        //dojo.stopEvent(e);
    },    
    keepMenu: function(e) {
        //console.log("calling keepMenu from "+e.target);
        clearTimeout(this.timer);
        Event.stop(e);
        //dojo.stopEvent(e);        
    },
    hide: function() { 
        $(this.menuId).addClassName("hidden");
    }
});


/**
 * brand.menuItem
 * line item in a menu
 * @memberOf brand.view
 */
brand.menuItem = Class.create({
    // domNode: Node Object
    // menu item dom node
    domNode: null,
    
    // rolloverClass: String
    // class to apply on mouseover
    rolloverClass: "",

    initialize: function(/* Object */args) { 
        this.domNode = args.domNode;
        this.rolloverClass = args.rolloverClass;
        if (this.domNode) {
            this.domNode.observe('mouseover', this._onMouseOver.bind(this));
            this.domNode.observe('mouseout',  this._onMouseOut.bind(this));
        }
    },
    _onMouseOver: function(e) {
        this.domNode.addClassName(this.rolloverClass);
    },
    _onMouseOut: function(e) {
        this.domNode.removeClassName(this.rolloverClass);
    }
});/**
 * brand.slideshow  
 * @memberOf brand
 */
brand.slideshow = Class.create({
    loop: false, 
    autoStart: false,  
    hasNav: false,  
    interval: 2, 
    looks: null,
    currentSlideIndex: -1,
    totalSlides: 0,
    slides: [],
    header: null,
    slide: null,
    link: null,
      
    initialize: function (args) { 
        if (!args.looks||!args.slide) return;
        //console.log("brand.slideshow");
     
        Object.extend(this, args || {});
        var self = this;   
     
        this.totalSlides = this.looks.length;
        var slide, look, header;
        for (var i=0;i<this.totalSlides;i++) {
            slide = {};
            slide.title = this.looks[i].title;
            //preload slides
            slide.slide = new Image();
            slide.slide.src = this.looks[i].image;
            slide.slide.link = this.looks[i].link;  
            //preload headers 
            slide.header = new Image();
            slide.header.src = this.looks[i].header;
            this.slides.push(slide); 
        };
     
        this.hasNav = (this.nav && this.nav.left && this.nav.right);
        if (this.hasNav) {
            if (this.looks.length==1) {
                if (this.nav && this.nav.left) this.nav.left.style.visibility = "hidden";
                if (this.nav && this.nav.right) this.nav.right.style.visibility = "hidden";  
            } else { 
                if (this.nav.left) this.nav.left.observe("click", function() {self.changeSlide(1)} );
                if (this.nav.right) this.nav.right.observe("click",function() { self.changeSlide(-1)} );
            } 
        } 
     
        this.changeSlide(1); 
    }, 
    changeSlide: function(n) {
        this.currentSlideIndex += n;
        if (this.currentSlideIndex > (this.totalSlides-1)) {
            if (this.loop) {
                this.currentSlideIndex = 0;
            } else {
               //hide next btn?
               return;  
            } 
        } else if (this.currentSlideIndex < 0) {
            if (this.loop) {
                this.currentSlideIndex = this.slides.length - 1;
            } else {
               //hide prev btn?
               return;  
            } 
        }
           
        this.setSlide();
    },
    setSlide: function() { 
        console.log("brand.slideshow.changeSlide: "+this.currentSlideIndex);
        this.slide.src = this.slides[this.currentSlideIndex].slide.src;
        if (this.header) this.header.style.backgroundImage = "url("+ this.slides[this.currentSlideIndex].header.src + ")"; 
        if (this.link) this.link.href = this.slides[this.currentSlideIndex].slide.link; 
        var headerLinkNode = $("slideshow_header_link");
        if (headerLinkNode) headerLinkNode.href = this.slides[this.currentSlideIndex].slide.link;
    }
});
/**
 * brand.progress
 * Class: Swap content with progress indicator while something loads
 * @memberOf brand
 */ 
brand.progress = Class.create(
{
    progressNode: null, // node to show on progress start
    containerNode: null, // node to hide on progress start 

    /**
     * brand.progress.initialize
     * Method for creating an instance of brand.progress
     * @returns An instance of brand.progress
     * @param {DOM Node} args.containerNode  Node object of content to hide during progress (Required: either args.containerNode or args.containerId)
     * @param {String} args.containerId  Id of content node to hide during progress
     * @param {DOM Node} args.progressNode  Node object containing progress indicator to show during progress (Required: either args.progressNode or args.progressId)
     * @param {String} args.progressId  Id of node containing progress indicator to show during progress
     * @param {Boolean} *Optional* args.matchDimensions Set to true if progress indicator content should automatically set its dimensions to match the content being hidden
     * @example
        var progress = new brand.progress({
            containerNode: swatchSet.domNode,
            progressNode: searchNodes.container.select('.progress')[0]
        }); 
     * @methodOf of brand.progress
     */    
    initialize: function(/* Object */args) {
        brand.updateProperties.apply(this, [args]);
        this.containerNode = (args.containerNode ? args.containerNode : $(args.containerId));
        this.progressNode = (args.progressNode ? args.progressNode : $(args.progressId));
        if (args.matchDimensions) {
            this._setDimensions();
        }
    },
    
    start: function() {
        if (!this.progressNode || !this.containerNode) { return; }
        this.containerNode.style.display = "none";
        this.progressNode.style.display = "block";
    },
    
    clear: function() {
        if (!this.progressNode || !this.containerNode) { return; }
        this.containerNode.style.display = "block";
        this.progressNode.style.display = "none";
    },

    // for errors and user messages
    showMessage: function(args) {
        if (!this.progressNode) { return; }
        this.progressNode.style.display = "none";
        var messageNode = args.messageNode;
        if (messageNode && args.message) {
            messageNode.update(args.message);
            messageNode.show();
        }
        if (!args.hideContainer && this.containerNode) {
            this.containerNode.style.display = "block";
        }
    },
    
    _setDimensions: function() { 
        this.progressNode.style.width = this.containerNode.getWidth() + "px";
        this.progressNode.style.height = this.containerNode.getHeight() + "px";    
    }
});

/**
 * brand.progressOverlay
 * Class: Display progress indicator as an overlay
 * @memberOf brand
 */
brand.progressOverlay = Class.create (brand.progress, 
{

    // summary:
    //      Progress overlay
    
    offset: {w: 0, h: 0},

    /**
     * brand.progressOverlay.initialize
     * Method for creating an instance of brand.progressOverlay
     * @returns An instance of brand.progressOverlay
     * @param {String} args.containerId  Id of content node to hide during progress
     * @param {String} args.progressId  Id of node containing progress indicator to show during progress
     * @param {Object} args.offset  width & height offset for dimensions of overlay
     * @example
        var progress = new generic.progressOverlay({
            containerId: "address-container",
            progressId: "progress-address-container",
            offset: {w: 10, h: 10}
        }); 
     * @methodOf of brand.progress
     */    
    initialize: function(/* Object */args) {
        this.containerNode = $(args.containerId);
        this.progressNode = $(args.progressId);
        if (args.offset) {
            this.offset = args.offset;
        }
        
        // set dimensions 
        this.progressNode.style.width = (this.containerNode.getWidth() + this.offset.w) + "px";
        this.progressNode.style.height = (this.containerNode.getHeight() + this.offset.h) + "px";
    },
    
    start: function() {
        if (!this.progressNode) { return; }
        this.progressNode.style.display = "block";
    },
    
    clear: function() {
        if (!this.progressNode) { return; }
        this.progressNode.style.display = "none";
    }
});/**
 * @namespace
 */
brand.forms = {
    init: function() {
        brand.forms.form.init();
        brand.forms.input.init();
    }
}

/**
 * @namespace
 */
brand.forms.form = {
    init: function() {
        // apply form submission behavior for specified forms
        $$("FORM.no-submit").each(function($_) { 
            $_.observe("submit", function(e) {
                Event.stop(e);
            }) 
        })
    } 
}

/**
 * @namespace
 */
brand.forms.input = {
    init: function() { 
        // apply inline label behavior for specified input fields
        $$("INPUT.inline-label-field").each(function($_) { 
            var fieldlabel = new brand.forms.inlineLabelField({ field: $_ });         
        });
    }
}


/**
 * This function tests a string against the expected pattern for an email address
 * @return {Boolean}
 * @param {String} Email address string to validate
 * @methodOf brand.forms
 */
brand.forms.isEmailAddress = function(s) {
    var filter  = /^([a-zA-Z0-9_\.\-])+\@(([a-zA-Z0-9\-])+\.)+([a-zA-Z0-9]{2,4})+$/;
    return (filter.test(s)); 
}

/* USED?
brand.forms.limitTextLength = function(args) {
    var field = args.field;
    var max_length = args.max_length;
    var display_element = args.display_element;
    
    if (field.value.length > max_length) {
        field.value = field.value.slice(0, max_length);
    }   
    if (!display_element) return;
    
    var remaining_chars = max_length - field.value.length; 
    if (remaining_chars < max_length) {
        // TODO: need RB/language solution here
        (remaining_chars != 1) ? plural = 's' : plural = '';
        display_element.innerHTML =  remaining_chars + ' character' + plural + ' left.';
    } else {
        display_element.innerHTML = '';
    }
}
*/


/**
 * brand.forms.inlineLabelField 
 * This class attaches behavior to a text or password input field where the label
 * is displayed inside the field and toggles on and off in response to focus/blur
 * @memberOf brand.forms
 */
brand.forms.inlineLabelField = Class.create({
    // summary:
    //      Handles toggling of label display for fields that show label
    //      in the field itself (via value attribute)

    // field: DOM node
    field: null,
    
    // fieldPswdDisplay: DOM node
    // field which displays password label
    // (workaround for IE's inability to toggle field's "type" attribute)
    fieldPswdDisplay: null,
    
    // label: String
    // field label
    label: "",
    
    // ftype: String
    // field type attribute (text, password)
    ftype: "text",
    
    // hasValue: Boolean
    // flag is true when user has entered a value in field
    hasValue: false,

    // _hasMaxlengthDisplay: Boolean
    // item's label char count exceeds field maxlength property
    _hasMaxlengthDisplay: false,
    
    // _maxlength: Object
    // if hasMaxlengthDisplay, _maxlength.data = maxlength property of input data, _maxlength.display = maxlength count for label
    _maxlength: null,

    /**
     * Creates an instance of brand.forms.inlineLabelField
     * @example
        var fieldlabel = new brand.forms.inlineLabelField({ field: fieldNode });

     * @param {Object} args.field  Input field node object   
     * @param {String} *Optional* args.label  Label string
     * @methodOf brand.forms
     */    
    initialize: function(/* Object */args) {
        this.field = args.field;
        
        // optional label arg
        if (args.label) {
            this.label = args.label;
        } else if (this.field.title && this.field.title.length > 0) {
            this.label = this.field.title;
        } else {
            this.label = "";
        }
        
        var displayField = this.field;
        this.ftype = this.field.getAttribute("type");
        
        if (this.ftype === "password") {
            this.fieldPswdDisplay = $(this.field.id+".label");
            if (!this.fieldPswdDisplay) { return };
            displayField = this.fieldPswdDisplay;
        }
        
        // check for maxlength restrictions
        var flength = this.field.maxLength;
        var labelLength = this.label.length;
        if ((flength > 0) && (flength < labelLength)) {
            this._maxlength = {};
            this._hasMaxlengthDisplay = true;
            this._maxlength.data = flength;
            this._maxlength.display = labelLength;
        }
        var self = this;
        this.handlers = [
            displayField.observe("focus", self._onFocus.bind(self)),
            this.field.observe("blur",self._onBlur.bind(self))
        ];
        
        this.setLabelState();
    },

    setLabelState: function() {
        this.checkHasValue();
        // only clear field if there is no user-set value in field
        if (!this.hasValue) {
            this.showLabel();
        }
    },

    checkHasValue: function() {
        var val = this.field.value;
        // field has user-set value
        if ((val.length > 0) && (val !== " ") && (val !== this.label)) {
            this.hasValue = true;
        } else {
            this.hasValue = false;
        }
    },
    
    beforeSubmit: function() {
        // if value is still populated by label rather than user input, clear it out before form submission
        if (!this.hasValue) {
            this.checkHasValue();
            if (!this.hasValue) {
                this.field.value = "";
            }
        }
    },
    
    _onFocus: function() {
        // display label if there is no user-set value in field
        if (!this.hasValue) {
            this.clearField();
        }
    },
    
    _onBlur: function() {
        this.setLabelState();
    },
    
    showLabel: function() {
        if (this.ftype === "password") {
            this.field.style.display = "none";
            this.fieldPswdDisplay.style.display = "block";
        } else {
            // set maxlength to accomodate label
            if (this._hasMaxlengthDisplay) {
                this.field.maxLength = this._maxlength.display;
            }
            // show label as value
            this.field.value = this.label;
        }
    },
    
    clearField: function() {
        if (this.ftype === "password") {
            this.field.style.display = "block";
            this.field.focus(); // set focus before currently focused field is hidden to avoid FF cursor disappearance
            this.fieldPswdDisplay.style.display = "none";
        } else {
            // clear field
            this.field.value = "";
            // set maxlength for expected data
            if (this._hasMaxlengthDisplay) {
                this.field.maxLength = this._maxlength.data;
            }
        }
    }
});

/**
 * brand.forms.contextualOptions 
 * This class attaches behavior to 2 select menus where the contents of the 2nd menu
 * are contextual based on the selected value of the 1st menu
 * @memberOf brand.forms
 */
brand.forms.contextualOptions = Class.create({ 
    srcSelect: null,
    targetSelect: null, 
    valueKey: null,
    labelKey: null, 
    targetSelectOptions: {}, 
    _selectsAreWidgets: false,

    /**
     * Creates an instance of brand.forms.contextualOptions
     * @example
        var contextualStoreSelect = new brand.forms.contextualOptions({
            srcSelectId: "select-menu-state",
            targetSelectId: "select-menu-stores",
            targetSelectOptions: {
               "AL" : [
                  {
                     "DOOR_NAME" : "The Summit",
                     "DOOR_ID" : "77790"
                  }
               ],
               "AK" : [
                  {
                     "DOOR_NAME" : "Lakeside",
                     "DOOR_ID" : "77771"
                  }
               ]
            }
            valueKey: "DOOR_ID",
            labelKey: "DOOR_NAME"
        });

     * @param {String} args.srcSelectId  ID of source select menu where initial selection will be made   
     * @param {String} args.targetSelectId  ID of target select menu whose options will be populated based on the selected value of args.srcSelectId 
     * @param {Object} args.targetSelectOptions  A JSON list of options for targetSelectId, keyed by the values listed the source select menu
     * @param {String} args.valueKey  Key ID for the option value in each item in the args.targetSelectOptions data
     * @param {String} args.labelKey  Key ID for the option label in each item in the args.targetSelectOptions data 
     * @memberOf brand.forms
     */     
    initialize: function(/* Object */args) {
        srcSelect = $(args.srcSelectId);
        targetSelect = $(args.targetSelectId);
        if (srcSelect && targetSelect) {
            srcSelect.observe("change", this.onChange.bind(this));
        } else {
            console.log("brand.form.contextualOptions: select element not found");
            return false;
        } 
        this.srcSelect = srcSelect;
        this.targetSelect = targetSelect;
        this.targetSelectOptions = args.targetSelectOptions; 
        if (args.valueKey) { this.valueKey = args.valueKey; }
        if (args.labelKey) { this.labelKey = args.labelKey; }
    },
    
    onChange: function() {
        var srcval = this.srcSelect.value;
        var targetSelect = this.targetSelect;
        if (srcval === "" || srcval === null) { return false; } 
        this.removeOptions(targetSelect);
        this.addOptions(targetSelect);
    },
    
    getNewOptions: function() {
        // summary:
        // 
        // Defines default path to list of new options
        // Redefined to pass list from different array/object heirarchy
        var selected = this.srcSelect.value;
        return this.targetSelectOptions[selected];
    },

    addOptions: function(targetSelect) {
        var isWidget = this._selectsAreWidgets;
        var options = this.getNewOptions();
        
        // expect value, label pair stored in data, either as object (key id'ed) or in array (first 2 positions)        
        var labelKey = (this.labelKey ? this.labelKey : 0);
        var valueKey = (this.valueKey ? this.valueKey : 1);

        options.each(function(option, ix) {
            targetSelect.options[ix] = new Option(option[labelKey], option[valueKey]); 
        });

    },
    
    removeOptions: function(targetSelect) {
        var l = (targetSelect.options.length - 1);
        var i;
        for (i = l; i >= 0; i--) {
            if (targetSelect.options[i].value !== "") {
                targetSelect.options[i] = null;         
            }
        }    
    }
    
});/**
 * @class brand.overlay 
 * Singleton class offers pop-over display functionality.
 * It supports one visible window at a time.
 *
 * Extension of brx.overlay. Additional features for Mac:
 * - showing pre-constructed popover (vs. creating a container for the content dynamically & inserting that into the DOM)
 * - args.foregroundNode
 * - args.removeOnHide
 * - args.displayDuration
 * - args.displayInline 
 */
brand.overlay = function() {
    var isVisible = false;
    var isCentered = true;
    var backgroundNode = null;
    var foregroundNode = null;
    var containerNode = null;
    var anchorNode = null;
    var offsetLeft = 0;
    var offsetTop = 0;
    var removeOnHide = true;
    var displayDuration = null;
    var durationTimer = null;
    var onClose = null;

    var scaleElementToPage = function(ele) {
        if (!Object.isElement(ele)) {
            return;
        }
        var docsize = $(document.body).getDimensions();
        var windowDimensions = document.viewport.getDimensions();
        docsize = (docsize.height > windowDimensions.height ? docsize : windowDimensions);        
        ele.setStyle({
            height: docsize.height+'px',
            width: docsize.width+'px'
        });        
    };

    var centerElement = function(ele) {
        if (!Object.isElement(ele)) {
            return;
        }        
        var contentDimensions = ele.getDimensions();
        var windowScrollOffsets = document.viewport.getScrollOffsets();
        var windowDimensions = document.viewport.getDimensions();
        if (global.isipad) windowScrollOffsets.left = 0;
        var yPosition;
        if (windowDimensions.height < contentDimensions.height) {
            yPosition = 0;
        } else {
            yPosition = (windowDimensions.height/2) - (contentDimensions.height/2) + (windowScrollOffsets.top);
            // ipad: make sure that top position is inside bounds of page content (e.g. zoom mode)
            if (global.isipad && ((yPosition + contentDimensions.height) > windowDimensions.height)) {
                var yPositionDiff = ((yPosition + contentDimensions.height) - windowDimensions.height);
                yPosition = (yPosition - yPositionDiff > 0 ? yPosition - yPositionDiff : 0);
            }
        }
        ele.style.top = yPosition + "px";
        var xPosition = (windowDimensions.width/2) - (contentDimensions.width/2) + (windowScrollOffsets.left);
        ele.style.left = xPosition + "px";
    };

    var scrollHandler = function(evt) {
        if (isCentered) {
            centerElement(foregroundNode);            
        } else if (anchorNode) {
            foregroundNode.clonePosition(anchorNode, {setWidth: false, setHeight: false, offsetLeft: offsetLeft, offsetTop: offsetTop});
        }
    };
    var insertCloseLink = function(containerEle) {
        var closeLink = new Element("a", {"class": "close-link"});
        closeLink.insert(generic.rb.language.rb_close);
        var closeDiv = new Element("div", {"class": "close-container"});
        closeDiv.insert(closeLink);
        containerEle.insert({"top": closeDiv});
        closeLink.observe("click", function (closeClickEvt) {
            closeClickEvt.preventDefault();
            brand.overlay.hide();
        });
        return closeLink;
    };
    var hideSelects = function () {
        var selectNodes = $$("select");
        selectNodes.each( function(node) {
            node.addClassName("overlay-hidden");
        });
    };
    var restoreSelects = function() {
        var selectNodes = $$("select.overlay-hidden");
        selectNodes.each( function(node) {
            node.removeClassName("overlay-hidden");
        });
    };

    return {
        /**
         * This function displays a pop-over window. If a pop-over is already showing, the launch()
         * function will replace it with the new one.
         * @example 
         * // brand.overlay.launch({
         * //     content: htmlNode,
         * //     cssStyle: {
         * //         border: #000000 1px solid,
         * //         backgroundColor: #ffffff,
         * //         left: "100px",
         * //         top: "350px",
         * //         width: "250px",
         * //         height: "350px"
         * //     },
         * //     cssClass: 'product-overlay',
         * //     lockPosition: true,
         * //     includeBackground: false
         * // });
         * @param {Object} args.cssStyle Hash of CSS style definitions for the window. Uses JS notation (i.e., "marginLeft").
         * @param {Object} args.cssClass Name of CSS class that is optional to add to the overlay window.
         * @param {String|Node} args.content HTML, node, or text that will display in the window.
         * @param {Boolean} lockPosition if true, the overlay layer will remain anchored at the same x,y coords
         * when the user scrolls or resizes.
         * @param {Boolean} includeBackground if true, a background Node will cover the page directly behind the
         * overlay contents.
         * @param {Boolean} args.center, no value (undefined) centers the content, setting this to 'false' allows 
         * a top and left css value. 'undefined' was used to prevent having to add a 'true' value to every overlay
         * @param {Node} args.lockToDomNode node whose position will be inherited by the overlay element.
         * @param {Number} args.offsetTop vertical offset from lockToDomNode node. Only applies if lockToDomNode is included.
         * @param {Number} args.offsetLeft horizontal offset from lockToDomNode node. Only applies if lockToDomNode is included.
         * @param {String|Node} args.foregroundNode overlay container node, if it already exists in the DOM and doesn't need to be created 
         * @param {Boolean} args.displayInline keep overlay content in its specified place in the DOM. Default is false.
         * @param {Boolean} args.removeOnHide if true, remove the overlay foreground Node from DOM on hide. Default is true. 
         * @param {Number} args.displayDuration milliseconds to show ovleray, after which the overlay the overlay is closed/hidden
         */
        launch : function(args) {
            if (isVisible) { // check internal flag
                this.hide();
            }
            
            // set variables per args & reset undefined params to defaults
            removeOnHide = ((typeof args.removeOnHide !== "undefined") ? args.removeOnHide : true);
            onClose = (Object.isFunction(args.onClose) ? args.onClose : null);
            displayDuration = args.displayDuration;
            foregroundNode = (args.foregroundNode ? args.foregroundNode : null);
            
            if (!removeOnHide && foregroundNode) {
                // if overlay is not set to be destroyed on hide, check or set class name that 
                // identifies whether overlay behavior has been hooked up
                if (foregroundNode.hasClassName("overlay-created")) {
                    this.show(args);
                } else {
                    this.create(args);
                    foregroundNode.addClassName("overlay-created");
                }
            } else {
                this.create(args);
            }
            
            hideSelects();
            isVisible = true; // set internal flag
            
            if (displayDuration) {
                var self = this;
                var reset = function() {
                    clearTimeout(durationTimer);
                    self.hide();
                }
                durationTimer = setTimeout(reset, displayDuration);
            } else {
                clearTimeout(durationTimer);
            }
        },
        create : function(args) {
            var displayInline = (args.displayInline ? args.displayInline : false);
            
            if (!containerNode && !displayInline) { // can't retrieve body variable in function declaration b/c it hasn't finished loading
                containerNode = $(document.body);
            }
            if (args.includeBackground) {
                if (!backgroundNode) { // create background node, if necessary
                    backgroundNode = new Element('div', {"class":"overlay-background", style:"display:none"});
                    containerNode.insert(backgroundNode);
                }
                backgroundNode.style.display = "block";
                scaleElementToPage(backgroundNode);
            }
            if (!foregroundNode) { // create foreground node, if necessary
                foregroundNode = new Element('div', {"class":"overlay-container", style:"display:none"});
                containerNode.insert(foregroundNode);
            }
            // insert elements into DOM if needed and adjust layout
            if (args.content) {
                foregroundNode.insert(args.content);
            }
            foregroundNode.style.display = "block";
            if (args.cssStyle) {
                foregroundNode.setStyle(args.cssStyle);
            }
            if (args.cssClass) {
                foregroundNode.addClassName(args.cssClass);
            }
            var closeLinks = foregroundNode.select(".close-link"); // look for a close link
            if (closeLinks.length < 1) {
                var newCloseLink = insertCloseLink(foregroundNode); // Insert link if one is not found
                closeLinks.push(newCloseLink);
            }
            var self = this;
            closeLinks.each( function(link) { // attach event handler to close links
                link.observe("click", function(clickEvt) {
                    self.hide();
                });
            });
            // attach events for scroll & resize
            // ipad workaround: don't centering on an ipad to avoid non-usable behavior in zoom mode
            if (!args.lockPosition && !displayInline && (global && !global.isipad)) {
                Event.observe( window, 'resize', scrollHandler );
                Event.observe( window, 'scroll', scrollHandler );
            }
            if (args.center == undefined && !displayInline) {
                isCentered = true;
            } else {
                isCentered = !!args.center;
            }
            if (isCentered) {
                centerElement(foregroundNode);
            }
            if (Object.isElement(args.lockToDomNode)) {
                if (args.offsetLeft) {
                    offsetLeft = args.offsetLeft;
                } else {
                    offsetLeft = 0;
                }
                if (args.offsetTop) {
                    offsetTop = args.offsetTop;
                } else {
                    offsetTop = 0;
                }
                anchorNode = args.lockToDomNode;
                foregroundNode.clonePosition(anchorNode, {setWidth: false, setHeight: false, offsetLeft: offsetLeft, offsetTop: offsetTop});
            } else {
                anchorNode = null;
            }
        }, 
        /**
         * This function shows a pop-over that's already been created and 
         * just needs hide/show toggling
         */
        show: function() {
            if (Object.isElement(foregroundNode)) {
                foregroundNode.style.display = "block";
            }
        },
        /**
         * This function "closes" the pop-over window. If this.recreateOnShow 
         * is set to true: It completely removes the foreground node and its children
         * from the DOM.  Otherwise, foregroundNode is just hidden.
         * The background element is set to display: none.
         */
        hide: function() {
            isVisible = false; // set internal flag
            restoreSelects();
            // clean up DOM and layout
            if (Object.isElement(foregroundNode)) {
                // MERGE NOTE: want option of removing or just hiding overlay
                if (removeOnHide) {
                    // remove events for scroll & resize
                    Event.stopObserving( window, 'resize', scrollHandler );
                    Event.stopObserving( window, 'scroll', scrollHandler );
                    // remove overlay node
                    foregroundNode.remove();
                    foregroundNode = null;
                } else {
                    foregroundNode.style.display="none";
                }
            }
            if (Object.isElement(backgroundNode)) {
                backgroundNode.style.display="none";
            }
            if (onClose) onClose();
        },
        /**
         * This function scans the DOM for <a class="overlay-links"> elements. It takes the href attribute
         * from those links and preloads that URL via AJAX into a hidden DIV. This div is used
         * as the content for an overlay window when the link is clicked.
         */
        initLinks: function() {
            var linksToModify = $$("a.overlay-link");
            linksToModify.each( function(link) {
                if (link.hasClassName("overlay-ready")) {
                    return;
                }
                var styleObj = {};
                var linkClassNames = link.className;
                var cssClass = "";
                var widthRegexResults = linkClassNames.match(/overlay-width-(\d+)/);
                if (widthRegexResults) {
                    styleObj.width = widthRegexResults[1] + "px";
                }
                var heightRegexResults = linkClassNames.match(/overlay-height-(\d+)/);
                if (heightRegexResults) {
                    styleObj.height = heightRegexResults[1] + "px";
                }
                var cssClassRegexResults = linkClassNames.match(/overlay-addclass-([a-z\-\_]+)/);
                if (cssClassRegexResults) {
                    cssClass = cssClassRegexResults[1];
                }
                
                var containerDiv = new Element("div");
                containerDiv.style.display = "none";
                document.body.appendChild(containerDiv);
                containerDiv.addClassName("overlay-content-container");
                
                var req = new Ajax.Request(link.href, {
                    method:'get',
                    onSuccess: function(transport) {
                        var response = transport.responseText || "no response text";
                        containerDiv.update(response);
                    },
                    onFailure: function(){
                        var errMsg = "Error loading " + link.href
                        containerDiv.update(errMsg);
                    }
                });
                link.observe("click", function(clickEvt) {
                    clickEvt.preventDefault();
                    containerDiv.style.display = "block";
                    brand.overlay.launch({
                        content: containerDiv,
                        includeBackground: true,
                        cssStyle: styleObj,
                        cssClass: cssClass
                    });
                });
                link.addClassName("overlay-ready");
            }); // end linksToModify.each()
            
        },
        /**
         * This function is used to fetch specific rb keys needed for the
         * overlay. It is called on dom::loaded so each key is only 
         * fetched once.  
         * The specific language bundle is needed to be included in each page 
         * template header.  If not, the key name called will be returned.
        */
        getRBKeys: function() {
            generic.rb.language = generic.rb("language");
            generic.rb.language.rb_close = generic.rb.language.get('close');
        }
    };
}();
var brand = brand || {};

/**
 * brand.img 
 * This class creates and preloads an image object for rollovers, src switching, etc...
 * @memberOf brand
 */
brand.img = Class.create({
     /**
     * @return {Object} instance of brand.img with the following methods available: changeSrc
     * @example
            var imgNode = link.select('img')[0];
            var imgObj = new generic.img(imgNode, ["on", "off"]);
            imgObj.changeSrc("on");
     * @param {Node} imgNode   
     * @param {Array} states  array of states/versions of the image, by standardized suffix, such as "on"
     * @methodOf brand.img
     */ 
    initialize: function(imgNode, states) {
        if (!imgNode) {
            console.log("brand.img: imgNode UNDEFINED");
            return false;
        }
        //console.log("brand.img: called on imgNode "+imgNode.id);
        this.node = imgNode;
        this.preloaded = {};
        // get filename bits 
        var src = this.node.src;
        var bits = src.match(/^(.*)_(on|off|sel|dis)\.(.*?)$/);
        this.suffix = "";

        if ( bits == null ) {
            var bits = src.match(/^(.*)_(on|off|sel|dis)_(.*?)\.(.*?)$/); // look for suffix between on|off & extension, ex: base_on_fr_ca.gif
            if ( bits == null ) { return false; }
            this.suffix = "_" + bits[3];
        }

        this.srcBase = bits[1];
        this.srcExt = bits[bits.length-1];
        if (!this.srcExt) return false;
        this.states = states;

        // preload img
        // note: version arg must correspond to suffix of image, i.e.:
        // if default image name is image_name_off.jpg, arg value "on" looks for image_name_on.jpg
        if (!this.states) return;
        for (var i=0;i<this.states.length;i++) {
            var state = this.states[i];
            var separator = (state !== "" ? "_" : "");
            var preloadSrc = this.srcBase + separator + state + this.suffix + '.' + this.srcExt;
            var pl = new Image();               
            pl.src = preloadSrc; 
            this.preloaded[state] = pl;
        }
    },
    changeSrc: function(state) {
        var p = (this.preloaded ? this.preloaded[state] : null);
        if (!this.srcBase) return;
        // use preloaded src ref
        if (p) {
            this.node.src = p.src;
            
        // else load img via path
        } else {
            this.node.src = this.srcBase + '_' + state + this.suffix + '.' + this.srcExt;
        }
    }
});

/**
 * brand.rollover 
 * Applies image switching behavior to an image node on mouseover/out of the specified
 * image node or (if specified) to a different trigger node
 * @memberOf brand
 */
brand.rollover = Class.create({
     /**
     * Creates instances of brand.rollover
     * @example
            var rollover = new generic.rollover($("my-image"), $("my-button")); 
     * @param {Node} imgNode   image whose src will be changed on mouseover/out
     * @param {Node} triggerNode  *Optional* target node of mouse event.  Default is imgNode.
     */
    initialize: function(/* Object */imgNode,/* Object */triggerNode){
        var trigger = (triggerNode == null ? imgNode : trigger);
        this.img = new brand.img(imgNode, ["off","on"]);
        trigger.observe("mouseover", this.onMouseOver.bind(this));
        trigger.observe("mouseout", this.onMouseOut.bind(this));
    },
    onMouseOver: function(e) {
        if (!e.target.hasClassName("disable_rollover")) {
            this.img.changeSrc("on");
        }
    },
    onMouseOut: function(e) {
        if (!e.target.hasClassName("disable_rollover")) {
            this.img.changeSrc("off");
        }
    }
});

/**
 * Preloads and stores a batch of images for use with image src switching 
 * @return {Object} Hash of preloaded images
 * @param {DOM Node} args.node  Image node
 * @param {String} args.imagePath  Image file path to preload and store 
 * @param {Object} args.imageStore  Previously saved image hash to append to
 * @param {String} args.imageId  ID key for image in hash 
 * @memberOf brand
 */
brand.loadImage = function(args) {
    var imgStore = args.imageStore;
    var imgId = args.imgId;
    var imgPath = args.imagePath;
    var node = args.node;
    if (!node || (typeof(imgId) === "undefined") || !imgStore || !imgPath) {
        return imgStore;
    }
    imgId.toString();
    var preloadedImg = imgStore[imgId];
    
    // if first request for image, store it
    if (!preloadedImg) {        
        imgStore[imgId] = new Image();
        imgStore[imgId].src = imgPath;
        node.src = imgStore[imgId].src;
    
    // if 2nd+ request get preloaded image
    } else {
        node.src = preloadedImg.src;
    }
    return imgStore;
}/**
 * brand.pager
 * @memberOf brand
 */
brand.pager = Class.create( {

	list: [],
	current: 1,
	per_page: 10,

	initializer: function(args) {
		if (!args.list) { return; }
		Object.extend(this, [args]);
		this._init();
	},

	_init: function() {
		this.pages = Math.ceil(this.list.length / this.per_page);
		this.first = 1;
		this.last = this.pages;

		var self = this;
		this.from = (function() {
			return (self.current - 1) * self.per_page;
		})();
		this.to = (function() {
			var tmp = self.from + self.per_page;
			return (tmp <= self.list.length) ? tmp : self.list.length;
		})();

		this.prev = (function() {
			return self.hasPrev() ? self.current - 1 : self.current;
		})();
		this.next = (function() {
			return self.hasNext() ? self.current + 1 : self.current;
		})();
	},
	next: function() {
	    self = this; 
	    return self.hasNext() ? self.current + 1 : self.current;
	},
	previous: function() {
	     self = this;
	     return self.hasPrev() ? self.current - 1 : self.current;
	},
	setPage: function(p) {
		if (p && typeof(p) == "string") {
			p = parseInt(p);
		}
		if (p && p !== this.current) {
			this.current = p;
			this._init();
			return this.getPage();
		}
	},

	getPage: function() {
		return this.list.slice(this.from, this.to);
	},

	hasNext: function() {
		return (this.current < this.pages);
	},

	hasPrev: function() {
		return (this.current > 1);
	}

});
brand.livechat = {
    livechatPopArgs : {
        url: "/includes/live_chat_popup.tmpl",
        resizable: "no",
        scrollbars: "no",
        width: 611,
        height: 450,
        menubar: "no",
        location: "no"
    },
    init: function() {
        // multiple instances of live chat popup buttons
        brand.livechat.initLiveChatPopup("gnav_popup_live_chat");
        brand.livechat.initLiveChatPopup("popup_live_chat");
        brand.livechat.initLiveChatPopup("popup_live_chat_2"); // if mult links on same page    
    },
    initLiveChatPopup: function(btnId) {
        var params = this.livechatPopArgs;
        params["activator"] = btnId;
        var lc = new generic.popup(params);
    },
    openLiveChatPopup : function() { // for cms inline reference
        var lc = window.open(this.livechatPopArgs.url, 'live_chat',"resizable=0, scrollbars=0, width=" + this.livechatPopArgs.width +", height=" + this.livechatPopArgs.height); 
        generic.events.fire({ event:"livepopup:click", type:"cms" });
    }  
} 
 brand.search = Class.create({

    // config: Object
    // section data from site.globalnav.config[section]
    config: null,
    
    // isDefaultPanel: Boolean
    // true if search panel is the open/page panel
    isDefaultPanel: false,
    
    // resultsNode: DOM node
    // node container for results html
    resultsNode: null,

    // progressNode: DOM node
    // progress node to show while search is in progress    
    progressNode: null,

    // _hasContent: Boolean
    // false before initial search, true once search has run & there are child widgets to destroy
    _hasContent: false,
    
    // children: Array
    // widget ids for each search result     
    _children: [""],
        
    _defaultState: null,    
    _isSearching: false,
           
    initialize: function(args) {
        //console.log("brand.search.init");
       
        if (!args.config || !args.config.search) return;
        
        for (var arg in args) {
            this[arg] = args[arg];
        } 
        
        this.formField = $(this.config.search.formFieldId);
        var formSubmit = $(this.config.search.formSubmitId);
        if (!this.formField || !formSubmit) return;
        
        this._defaultState = page_data.panel_nav["default"];
      
        var psubnav = $("psubnav_" + this.config.id).widget;
        this.resultsNode = psubnav.resultsNode;
        this.contentResultsNode = psubnav.contentResultsNode;
        this.progressNode = (this.progressNode ? this.progressNode : psubnav.progressNode);
        
        this.formField.observe("keypress", this._onkeypress.bind(this));
        formSubmit.observe("click", this._onclick.bind(this));

        if (this.isDefaultPanel && this._defaultState.query) {
            this.submit({ query: this._defaultState.query });
        }
    },

    _onkeypress: function(event) { 
        if (event.keyCode != Event.KEY_RETURN) {
            return false;
        }                
        this.submit(event);      
    },
    
    _onclick: function(e) {
        this.submit(e);
    },
    
    submit: function(args) {
        if (this._isSearching) { return; }
        var query = (args && args.query) ? args.query : this.formField.value;
       
        // on error: show popover
        if ( !query || query === this._defaultState.searchDefault ) {
            var popid = this.config.search.errorPopup;
            if (popid) {
                brand.overlay.launch({
                    foregroundNode: $(popid),
                    displayInline: true,
                    removeOnHide: false,
                    displayDuration: 5000
                }); 
            }
            
            return false;
        }
         
        // if no error: execute search
        this._execute({ query: query });
    },

    _execute: function(args) {
        this._isSearching = true;
        var psubnav = $("psubnav_" + this.config.id).widget;        
        var defaultId = null;
        //console.log("brand.search._execute: "+this.parentId + " / " + $(this.parentId));
        this.reset();
        
        if (!$(this.parentId)) return; //TESTING
        
        // show progress, hide content
        this._showProgress(true);
        psubnav.resultsMessageNode.innerHTML = "";
        if (psubnav.contentResultsContainer) {
            psubnav.contentResultsContainer.addClassName("hidden");
        }
        // if triggered by form submit vs. panel click
        if (this.panelManagerId) {
            var gset = $(this.parentId).widget; 
            var pnavManager = gset.getChild(this.panelManagerId);
        }
  
        // check if default state        
        if (this.isDefaultPanel) {            
            defaultId = (this._defaultState.item.item ? this._defaultState.item.item.id : null); 
            if (this.panelManagerId) {
                this._showDefault();
            } 
        // search: open sliding panel
        } else if (pnavManager) {
            pnavManager.onTrigger(true);      
            window.parent.scrollTo(0,0); //scroll to top of screen
        }
                
        // ajax request
        var self = this;
        var onLoadArgs = {
            query: args.query,
            psubnav: psubnav,
            defaultId: defaultId
        }
        
        // wait for the panel to open before starting load (avoids animation stutters)
        // Panel.durationOpen = 400
        // TODO: create callback in Panel.js to know when panel has opened instead of using timeout
        var doRequest = function() {
            var c = self.config.content;
            var url = c.url + "?" + c.param + "=" + encodeURIComponent(args.query); 
            //var url = "/js/brand/globalnav/data/" + self.config.id + "_"+ args.query + ".txt";  
            new Ajax.Request(
                url, {
                method: 'get',
                onSuccess:  function(transport) {  
                     self.onLoad(transport.responseText, onLoadArgs)
                }.bind(this) 
             }); 
        }              
        setTimeout(doRequest, 400);
             
    },
    
    onLoad: function(data, args) {
        //console.log("brand.search.onLoad");

        this._isSearching = false;
        var self = this;
        var psubnav = args.psubnav; 
        
        if (typeof data == "string") data = data.evalJSON(true); 
        var detailItems = data.products;
        
        var hasItemInDefaultCategory = false; 
        var contentResults = data.content_results || [];
        var hasResults = (detailItems.length > 0 || contentResults.length > 0); 
        
        // reset previously rendered content/state
        if (this._hasContent) {
            this.reset();
        }
        // headers for results and no results
        //var hdNode = $(this.id + "_hd");
        var hdNode = $("psubnav_search_hd");
        if (hdNode && data.header_img) {
            hdNode.setAttribute("src", data.header_img);
            hdNode.setAttribute("alt", data.header_alt);
        }
        
        // show results count/message
        if (data.results_message) {
            psubnav.resultsMessageNode.innerHTML = data.results_message;
        } else if (!hasResults && data.no_results_message) {
            psubnav.resultsMessageNode.innerHTML = data.no_results_message;
        }
        
        if (!hasResults) {
            this._showProgress(false);
            return false;
        }
        
        // SS note: move this to where "/globalnav/event/getcontent/onload" used to fire?
        generic.events.fire({event:"search:results", msg:{ pageid: "Search", keywords: args.query, count: data.count, cat: "2200"}});
              
        // check if results contain default item 
        if (this.isDefaultPanel) {
           if (this._defaultState.query === args.query) {
                hasItemInDefaultCategory = detailItems.any(function(item){ 
                    return (item.id === args.defaultId);
                });
            }
  
            if (hasItemInDefaultCategory) {
                this.resultsNode.addClassName("panelnav_category_default");
            } else {
                 this.resultsNode.removeClassName("panelnav_category_default");
            }
        }
        
        // output content results, if they exist
        if (data.content_results) { 
             data.content_results.each( function(item, idx) {
                var detailArgs = {
                    idx: idx,
                    item: item
                };
                var detail = self._initContentSearchDetail(detailArgs);
                self._children.push(id);
                psubnav.addSubItem(detail.domNode, self.resultsNode);
                
                if (detail.startup) detail.startup(); 
            });
        }       

        detailItems.each( function(item, idx) {
            //console.log("brand.search.onLoad detailItems "+idx + "/" +  psubnav.id);
            // set if default
            var isdefault = false;
            if (!item.id) item.id = item.sku.path
            
            if (hasItemInDefaultCategory && (item.id === args.defaultId)) {
                isdefault = true;
            }
            var id = "psubitem_" + self.config.id + "_" + item.id;

            var detailArgs = {
                id: id,
                item: item,
                isdefault: isdefault,
                isInDefaultCategory: hasItemInDefaultCategory, 
                parentId: psubnav.id, 
                domParent: $(psubnav.id).widget.resultsNode
            }
           // discontinued search results
            if (self.config.id === "discontinued") {
                var detail = self._initDiscontinuedDetail(detailArgs);
                
            // main search results
            } else {
                var detail = self._initSearchDetail(detailArgs);         
            }

            self._children.push(id);
            
            // place detail node in DOM & start it up:
            if (detail.startup) detail.startup();  
        });
        
        // broadcast that search content has loaded/rendered:
        var pid;
        if (this.config.id === "search") {
            pid = "pnav_search_panel";
        }
        //JSTest dojo.publish("/globalnav/event/getcontent/onload", [{ type: "panel", id: pid, parentId: psubnav.parentId }]);
            
        this._hasContent = true; // nodes exist which will have to be destroyed before next search
        
        // hide progress, show content
        this._showProgress(false);   
    },

    _initSearchDetail: function(args) {
        //console.log("brand.search._initSearchDetail");
        var item = args.item;      
                
        var detailArgs = Object.extend(args, { 
            url: item.uri, 
            product: item,
            hdPath: item.header,
            //hdHeight: item.header_image_height,
            displayName: item.name,
            description: item.short_desc,
            thumbPath: item.thumb,
            hex: (item.shade_result ? item.sku.color[0] : ""),
            shadename: (item.shade_result ? item.sku.shade_name : "") 
        });
        
        if ( item.is_giftcard == 1 )  {
            detailArgs.templatePath = "jsTemplates.globalnav.SearchGiftcard";
            var detail = new brand.globalnav.SearchProductDetail(detailArgs);
        } else if ( item.is_custom_palette == 1 )  {
            detailArgs.templatePath = "jsTemplates.globalnav.SearchCustomPalette";
            var detail = new brand.globalnav.SearchProductDetail(detailArgs);        
        } else if ( item.shade_result || !item.shaded ) {
            var detail = new brand.globalnav.SearchQuickBuyDetail(detailArgs);
        } else {
            try {
                var detail = new brand.globalnav.SearchProductDetail(detailArgs);    
            } catch(err) {
                console.log("brand.search._initSearchDetail error " + err.description);
            }
        }           
            
        return detail;
    },

    _initContentSearchDetail: function(args) {
        // TODO: mac jp version has not been tested.  may be missing necessary args
        var id = "psubitem_" + this.config.id + "_content_" + args.idx;
        var item = args.item;        
        var detail = new brand.globalnav.Detail({
            id: id,
            templatePath: "jsTemplates.globalnav.SearchContentDetail",
            url: item.url,
            description: item.short_desc,
            isdefault: false,
            isInDefaultCategory: false
        });    

        return detail;
    },
    
    _initDiscontinuedDetail: function(args) {
        var item = args.item;   
        var detail = new brand.globalnav.DiscontinuedProductDetail({
            id: args.id,
            url: item.uri,
            displayName: item.name,
            hdPath: item.header,
            thumbPath: item.thumbnail,
            description: item.description,
            sku: item.sku,
            shadedResult: (item.shade_result == 1 ? true : false),            
            isdefault: args.isdefault,
            isInDefaultCategory: args.isInDefaultCategory
        });  
        
        return detail;
    },

    _showProgress: function(/* Boolean */state) {
        // toggle display of progress & content
        var s = this.progressNode.style;
        if (state) {
            s.display = "block";
            this.resultsNode.style.display = "none"; 
        } else {
            s.display = "none";
            this.resultsNode.style.display = "block";
        }
    },
    
    reset: function() {
        //console.log("brand.search.reset");
        this.resultsNode.innerHTML = "";
     
        this._showProgress(true);
        this._hasContent = false;
    },
    
    _showDefault: function() {
        var parent = $("globalnav_container").widget;
        // tell parent set to bring focus back to default
        if (parent.onChildClick && (parent.activeItemId !== "")) {
            parent.onChildClick(this.config.id, true);
        }
    }
    
});brand.customerService = {}

brand.customerService.faq = {
    activeId: null,
    
    init: function() {
        // get drop down                
        var menu = $("faq-questions");
        var self = this;
        self.showAnswer(menu.value);
        menu.onchange = function() {
            self.showAnswer(menu.value);
        }
    },
    showAnswer: function(questionId) {
        var activeNode = $(this.activeId);
        if (activeNode) {
            activeNode.style.display = "none";
        }
        var answerNode = $(questionId);
        if (answerNode) {
            answerNode.style.display = "block";
        }
        this.activeId = questionId;
    }
}
brand.product = brand.product || {};

// check for duos, trios quads, etc...
brand.product.getShadeType = function(args) {
    var type = "solo";
    var ismulti = false;
    var sku = args.product.skus[0];
    
    // check properties in page data
    var pdtype = args.product.product_multicolor_type;
    if (!pdtype && sku.sku_multicolor_type) {
        pdtype = sku.sku_multicolor_type;
    }

    // finally, check for multiple hexes
    if (pdtype && (sku.color.length >= args.multicolor_min)) {
        type = pdtype;
        ismulti = true;
    }
      
    return { type: type, ismulti: ismulti };
}


// inventory status display
// SPP: for shaded prods with cards: handles inventory message in card & display 
//      toggling of bag button in card  & spp page
// SPP: for prods w/out cards: handles inventory message in page & display
//      toggling of bag button in page
// MPP cartAdd popover: handles inventory message in popover & display toggling 
//      of bag button in popover
// arguments: 
//      shoppable: page_data sku "shoppable"
//      message: page_data sku "inventory_status_message"
//      messageNode: node in which to insert message string
//      buttonNode: specific bag button node to suppress (optional)  By default
//          brand.product.inventoryStatus hides all nodes w/ class "inventory-status-conditional"
//      containerNode: node to add "visible-inventory-status" css class to (optional)
brand.product.inventoryStatus = function(args) {
    var msgNode = args.messageNode;
    var statusKey = args.statusKey;
    var suppressAddToBag = (statusKey == 1 || statusKey === "1" || args.shoppable === "1" || args.shoppable == 1 ? false : true);
    var btnNodes = [];
    // use specified buttonNode
    var btn = $(args.buttonNode);
    if (btn) {
        btnNodes[0] = btn;
    } else {    
        // get all bag buttons with class="inventory-status-conditional"
        var btnNodes = $$(".inventory-status-conditional");
    }
    
    if (suppressAddToBag) {
        btnNodes.each(function(btn) { 
            btn.hide();
        });
    } else {
        btnNodes.each(function(btn) {
            btn.show();
        });
    }

    var containerNode = args.containerNode; // set css class on container for easier manipulation of inventory display states in css instead of in js
    // get & display message if any
    var message = args.message;
    if ((!message || message.length < 2) && statusKey) {
        if (site.product.rb["inventory_status_message_" + statusKey]) message = site.product.rb["inventory_status_message_" + statusKey];
    }

    if (message && message.length > 1) {
        if (msgNode) msgNode.update(message);
        if (containerNode) {
            containerNode.addClassName("visible-inventory-status"); // for spp
        } else if (msgNode) {
            msgNode.style.display = "block";
        }
    } else {
        if (msgNode) msgNode.update("");
        if (containerNode) {
            containerNode.removeClassName("visible-inventory-status");
        } else if (msgNode) {
            msgNode.style.display = "none";
        }
    }

}


/* brand.product.addButton
 * Adapted from brx.product.addButton
 */
brand.product.addButton = function (args) { 
    //console.log("add button args ",args);
    var addButtonNode = args.addButtonNode;
    var cat_base_id;
    var nodeId;
    var nodeValue;
    var matchResult;
    
    // for coremetrics: get cat_base_id either from the node id or value
    nodeId = addButtonNode.id;
    if (nodeId !== "" && typeof(nodeId) !== "undefined") {
        matchResult = nodeId.match("CAT[0-9]+");
        if (matchResult){
            cat_base_id = matchResult[0];
            cat_base_id = cat_base_id.match("[0-9]+")[0];
            //console.log("CAT BASE from id ",cat_base_id );
        }
    }
    if (!cat_base_id) {
        nodeValue = addButtonNode.value;
        if ( typeof nodeValue !== "undefined" && nodeValue !== "" ) {
            matchResult = nodeValue.match("CAT[0-9]+");
            if (matchResult) {
                cat_base_id = matchResult[0];
                cat_base_id = cat_base_id.match("[0-9]+")[0];
                //console.log("CAT BASE from value",cat_base_id );
            }
        }
    }

    if (!addButtonNode) return;
    var progressNode = args.progressNode;
    var skuData = args.skuData;
    var skus = [];
    var enabled = true;
    var callback = args.callback || function() {};
    var onFailure = args.onFailure || callback;
    var skuField;

    // sku values can be set before click for non-shaded or "add to all buttons"
    if (args.skuData && args.skuData.SKU_BASE_ID) {
        skus = [args.skuData.SKU_BASE_ID];
    } else if (args.skuBaseId) {
        skus = [args.skuBaseId];
    } else if (args.skus) {
        skus = args.skus;
    } else if (args.skuField) {
        skuField = args.skuField;
    } else if (addButtonNode.nodeName === "INPUT") {
        skuField = args.addButtonNode;
    } else {
        return null;
    }
    var itemType = args.itemType || 'cart';
    var action = args.action || 'add';
    
    if (progressNode) {
        var progress = new brand.progress({
            containerNode: addButtonNode,
            progressNode: progressNode
        });
    }
        
    addButtonNode.observe("click", function(clickEvt) {
        clickEvt.preventDefault();
        if (!enabled) return;
        var item_params = {};
        
        // set sku value for buttons that don't know sku value until selection (ex: shaded)
        if (skuField) {
            var sku = skuField.value;
            if (sku.indexOf("SKU") > -1) { // get sku base id from full path
                sku = sku.split("SKU")[1];
            }
            skus = [sku];
        }
        if (!skus || !skus[0]) return;

        if (progress) {
            progress.start();
        }
        enabled = false;
        
        // for coremetrics: if cat_base_id undefined at init, check value at click
        if (!cat_base_id && clickEvt.target && clickEvt.target.value){
            nodeValue = clickEvt.target.value;
            if (nodeValue) {
                matchResult = nodeValue.match("CAT[0-9]+");
                if (matchResult) {
                    cat_base_id = matchResult[0];
                    cat_base_id = cat_base_id.match("[0-9]+")[0];
                    //console.log("CAT BASE from value last chance",cat_base_id );
                }
            }
        }
        
        // Send individual parameters based on type
        item_params = {
            skus: skus,
            itemType: itemType,
            CAT_BASE_ID: cat_base_id
        }
        // MERGE NOTE: if correct syntax is mac JP, then adding multiple skus looks like it shouldn't use increment or action
        if (itemType !== 'favorites' && skus.length == 1) {
            item_params.INCREMENT = 1;
        }
        if (skus.length == 1) {
            item_params.action = action;
        }
        // MERGE NOTE: not sure if removal calls should be using qty = 0, or "delete" as action.  If the former, then when creating instance of addButton, pass qty instead of action
        //if (action === "delete") {
            //item_params.QTY = 0;
            //item_params.INCREMENT = -1;
        //}
        
        generic.checkout.cart.updateCart({
            params: item_params,
            onSuccess: function(r) {
                var resultData = r.getData();
                callback(r);
                if (progress) progress.clear();
                enabled = true;
                addButtonNode.fire("cartButton:success", resultData);
            },
            onFailure: function(r) {
                onFailure(r);
                if (progress) progress.clear(); 
                enabled = true;
            }
        });
    });
    return {
        getItemType: function() {
            return itemType;
        },
        setSkuBaseId: function (newSkuBaseId) {
            skuBaseId = newSkuBaseId;
        },
        setSkuData: function (data) {
            skuData = data;
            this.setSkuBaseId(data.SKU_BASE_ID);
        },
        setShoppable: function() {
            if (!skuData) {
                return null;
            }
            if (brx.productData.isShoppable(skuData)) {
                addButtonNode.removeClassName("hidden");                
            } else {
                addButtonNode.addClassName("hidden");                               
            }
        },
        setEnabled: function(state) {
            enabled = state;
        }
    };
};


// Shoppables on content pages (i.e. news, faves)
// Uses catalog data from page_data as rendered on initial page load (vs. brand.shoppableContent which uses the RPC call(s) to get product data).
brand.shoppableContentPD = {
    cartConfirmProps: {},
    
    init: function(args) {
        //console.log("brand.shoppableContentPD.init");
        var self = this;
        
        var is_shaded = false; 
        var containerId = (args.containerId ? args.containerId : "main_content");
        this.cartConfirmProps = (args.cartConfirmProps ? args.cartConfirmProps : this.cartConfirmProps);
        
        if (args.positionPopup) {
            this.positionPopup = args.positionPopup;
        }
         
        args.products.each(function( product ) {
            var skus = product.skus;
            skus.each(function( sku ) {
    
                var hasLinkNodes = false; 
          
                // check for link nodes as single instances (element  id) or multiple instances (css class)
                var linkNodes = [];
                linkNodes = $(containerId).select("."+sku.path);
                if (linkNodes[0]) {
                    hasLinkNodes = true;
                } else {
                    linkNodes[0] = $(sku.path);
                    if (linkNodes[0]) {
                        hasLinkNodes = true;
                    }
                }
                
                //console.log("brand.product.shoppableContent.init hasLinkNodes = "+hasLinkNodes+" sku = ",sku);
                            
                if (hasLinkNodes) {
                    self.initPopover(product, sku, linkNodes);
                }
            });            
        });         
    },
    
    initPopover: function(product, sku, linkNodes) {
        //console.log("brand.product.shoppableContent.initPopover");        
        var self = this;
        
        if ((sku.shade_name.length > 0) && (sku.color[0].length > 2)) {
            is_shaded = true;
        } else {
            is_shaded = false;
        }
        
        this.cartConfirmProps.type = "order";
        
        var cartConfirmPlaceholder = $("cart_confirm_placeholder-" + sku.path);
        var cartAddPlaceholder = $("cart_add_placeholder-" + sku.path);
        if (!cartConfirmPlaceholder || !cartAddPlaceholder) return; // placeholder popover nodes not found

        var cartConfirmMsg = new brand.product.cartConfirm({
            id: "cart_confirm-" + sku.path,
            is_shaded: false,
            prodName: product.name,
            nodeToReplace: cartConfirmPlaceholder,
            sku: sku
        });
        
        cartConfirmMsg.setDisplayProperties(this.cartConfirmProps);        
        // init quick buy popover
        var cartAddArgs = {
            id: "cart_add-"+sku.path,
            is_shaded: is_shaded,
            prodName: product.name,
            skuFieldId: "prod_sku_cart_add-" + sku.path,
            price: product.price,
            cartConfirm: cartConfirmMsg,
            sku: sku,
            nodeToReplace: cartAddPlaceholder
        }
        
        if (is_shaded) {
            cartAddArgs.smooshId = "smoosh_img_cart_add-" + sku.path // pass smoosh img id to set template to use swatch smoosh instead of product thumb
        } else {
            cartAddArgs.smooshPath = product.image_small;
        }
        
        var cartAddMsg = new brand.product.cartAdd(cartAddArgs);
        
        if ($("prod_sku_cart_add-" + sku.path)) $("prod_sku_cart_add-" + sku.path).value = sku.path; 
        
        var show = function(evt) {
            self.show(evt, cartAddMsg, cartConfirmMsg);
        }
        
        // connect each link node to popover action
        linkNodes.each(function( node ) {
            node.observe("click", show );  
        });
    },
    
    show: function(evt, cartAddMsg, cartConfirmMsg) {
        if (this.positionPopup) { 
            this.positionPopup(evt, cartAddMsg, cartConfirmMsg);
        }
        cartAddMsg.show();            
    }
};

// Shoppables on content pages (i.e. blogger's obsession, faves)
// Uses catalog data from RPC call (vs. brand.shoppableContentPD which uses page_data). 
brand.shoppableContent = {
    productFields: ["PRODUCT_ID", "PROD_RGN_NAME", "THUMBNAIL_IMAGE", "shaded"],
    skuFields: ["SKU_ID", "SHADENAME", "formattedPrice", "HEX_VALUE_STRING", "INVENTORY_STATUS", "PRODUCT_CODE", "PRODUCT_ID"],
    
    init: function(args) {
        //console.log("brand.shoppableContent.init");
        var productIds = [];
        var skuIds = [];
        var self = this;
        var prodidx = 0;
        var containerId = args.containerId || "main_content";
        var container = $(containerId);
        if (!container) return;
        this.overlayArgs = args.overlayArgs;

        container.select(".shoppable").each(function(node, idx) {
            //console.log("node = "+node);
            // get product & sku id from css class
            var cssString = node.className.split(" "); // get rid of any classes that some before or after sku path
            var skuPath;
            if (!cssString) return;
            if (cssString[0].indexOf("SKU")) {
                skuPath = cssString[0];
            } else {
                skuPath = cssString[1];
            }
            cssString = skuPath.split("PROD")[1];
            if (!cssString) return;
            cssString = cssString.split("SKU");
            var prodid = ("PROD" + cssString[0]);
            var skuid = ("SKU" + cssString[1]);
            //console.log("cssstring = "+cssString+" prodid = "+prodid+" skuid = "+skuid);
            if (prodid && skuid) {
                var duplicateProdIdx = productIds.indexOf(prodid);
                if (duplicateProdIdx == -1) { // filter out dup prodids
                    prodidx = productIds.length;
                    productIds.push(prodid);
                } else {
                    prodidx = duplicateProdIdx;
                }
                skuIds.push(skuid);
                node.addClassName("SKUID-"+skuid); // give node skuid so we can find it on click
                node.skuPath = skuPath; // save skupath to pass to overlay creation
            }

            // connect each link node to popover action
            //console.log("attaching click event to ",node);
            node.observe("click", function(event) {
                //console.log("node clicked : ",node.skuPath);
                event.preventDefault();
                self.initOverlay(event);
            }); 
        });

        this.productIds = productIds;
        this.skuIds = skuIds;
        this.getData();
        
        // TEST
        //this.productData = catprodTEST;
        //this.getItems(this.productData);
        // TEST
    },
    
    getData: function() {
        if (!this.productIds || !this.skuIds) return;
        var self = this;
        var jsonRpcParams = {};
        jsonRpcParams.products = this.productIds;
        jsonRpcParams.product_fields = this.productFields;
        jsonRpcParams.skus = this.skuIds;
        jsonRpcParams.sku_fields = this.skuFields;
        jsonRpcParams.suppress_bad_skus = 1;
        
        generic.jsonrpc.fetch({
            method: "prodcat",
            params: [jsonRpcParams],
            onSuccess: function(jsonRpcResponse) {
                //console.log("response = ",jsonRpcResponse.getValue());
                self.productData = jsonRpcResponse.getValue();
                self.getItems(self.productData);
            },
            onError: function (jsonRpcResponse) {
                console.log("prodcat fail: ",jsonRpcResponse);
            }
        }); 
    },
    
    // check the response for missing sku or prods that may have been suppressed as invalid
    getItems: function(productData) {
        if (!productData) return;
        var productIds = this.productIds;
        var skuIds = this.skuIds;
        var linkSkusContext = {};
        
        // create linkSkusContext array
        productData.skus.each(function(sku, idx) {
            linkSkusContext[sku.SKU_ID] = { skuidx: idx }
        });
        this.linkSkusContext = linkSkusContext;
        
        // match product items in response to skus in linkSkusContext
        productData.skus.each(function(sku) {
            var prodidx;
            productData.products.each(function(product, prodidx) {
                if (product.PRODUCT_ID === sku.PRODUCT_ID) {
                    linkSkusContext[sku.SKU_ID].prodidx = prodidx;
                }
            });
        });        
        
        this.linkSkusContext = linkSkusContext;
    },
    
    initOverlay: function(event) {
        //console.log("initOverlay, event target = "+event.target+" productData = "+this.productData);
        //console.log("this.linkSkusContext = ",this.linkSkusContext);
        var linkNode = event.target;
        var linkClassId = linkNode.className.split("SKUID-")[1];
        var linkObj = this.linkSkusContext[linkClassId];

        var productData = this.productData;
        if (!productData) return;

        var sku = productData.skus[linkObj.skuidx];                      
        var prod = productData.products[linkObj.prodidx];        
        if (!prod || !sku) return;

        var thumbPath = prod.THUMBNAIL_IMAGE;
        var shadeName, hex, finishName;
        
        if (prod.shaded == 1) {
            thumbPath = "/images/products/swatches/56x56/" + sku.PRODUCT_CODE + ".jpg";
            shadeName = sku.SHADENAME;
            hex = sku.HEX_VALUE_STRING;
            finishName = sku.finishName;
        }
        var skuData = sku;
        Object.extend(skuData, {
            prodName: prod.PROD_RGN_NAME,
            thumbPath: thumbPath,
            shadeName: shadeName,
            hex: hex,
            finishName: finishName,
            path: linkNode.skuPath
        });
        //console.log("skuData = ",skuData);
        
        site.shoppableContent.productOverlay.init({ skuData: skuData, linkNode: linkNode, overlayArgs: this.overlayArgs, clickEvent: event });
    }
};

// cart overlay for brand.shoppableContent
brand.shoppableContent.productOverlay = {

    overlayArgs: {
        isCentered: false,
        lockPosition: true  
    },
            
    init: function(args) {
        // default text
        this.text_addedMessageCheckout = site.product.rb.added_to_shopping_bag || "";        
        if (args.overlayArgs) Object.extend(brand.shoppableContent.productOverlay.overlayArgs, args.overlayArgs);
        this.clickEvent = args.clickEvent;
        this.initCartAdd(args.skuData, args.linkNode);
    },
    
    initCartAdd: function(skuData, linkNode) {
        var self = this;
        
        // args to pass to generic.overlay
        var overlayArgs = this.overlayArgs;
        overlayArgs.cssClass = "cart-add-overlay-container";
        overlayArgs.lockToDomNode = linkNode;

        // position overlay & populate overlay with content
        var render = function() {
            var btnNode = $('prod_sku_button');
            btnNode.style.visibility = "hidden"; // hide until set up
            var overlayContainerNode = $$('.cart-add-overlay-container')[0];
            if (!overlayContainerNode) return;
            
            // position overlay
            if (overlayArgs.lockToMouse) {
                self.positionToMouse(self.clickEvent, overlayContainerNode);
            } else if (overlayArgs.setPosition) {
                overlayArgs.setPosition(linkNode, overlayContainerNode);
            }
            
            // broadcast to Coremetrics
            generic.events.fire({event:"productmessage:cartadd/show", msg: skuData});
            
            var prodNameNode = overlayContainerNode.select('.popover-title')[0];
            prodNameNode.update(skuData.prodName);
            var thumbNode = overlayContainerNode.select('img.thumb')[0];
            thumbNode.src = skuData.thumbPath;
            thumbNode.alt = skuData.prodName;
            var priceNode = overlayContainerNode.select('.popover-price')[0];
            priceNode.update(skuData.formattedPrice);
            
            if (skuData.shadeName) {
                var shadenameNode = overlayContainerNode.select('.popover-shadename')[0];
                shadenameNode.update(skuData.shadeName);
                thumbNode.style.backgroundColor = skuData.hex;
            }
            if (skuData.finishName) {
                var finishNameNode = overlayContainerNode.select('.popover-finish')[0];
                finishNameNode.update(skuData.finishName);
            }
            
            addButton(overlayContainerNode, btnNode);

            // inventory status
            var inventoryStatusNode = overlayContainerNode.select('.inventory-status')[0];
            site.product.inventoryStatus({
                statusKey: skuData.INVENTORY_STATUS,
                messageNode: inventoryStatusNode,
                buttonNode: btnNode
            });
        };
    
        // add to cart button
        var addButton = function(overlayContainerNode, btnNode) {
            btnNode.value = skuData.path || skuData.sku.SKU_ID;
            // add to cart button
            var prodBtn = site.product.addButton({
                addButtonNode: btnNode,
                callback: function(cartAddResponse) {
                    //console.log("addbutton callback");
                    self.initCartConfirm(skuData, linkNode, cartAddResponse);
                }
            });
            
            // localize image
            self.regionalizeImage(btnNode);
        };

        var getTemplate = function() {
            generic.templatefactory.get({path: '/templates/products-cart-add.tmpl'}).evaluateCallback({
                callback: function(html) {
                    overlayArgs.content = html;
                    // open overlay
                    generic.overlay.launch(overlayArgs);
                    render();
                }
            });
        };
        
        getTemplate();
    },
    
    initCartConfirm: function(skuData, linkNode, cartAddResponse) {
        var self = this;
        
        // args to pass to generic.overlay
        var overlayArgs = this.overlayArgs;
        overlayArgs.cssClass = "cart-confirm-overlay-container";
        overlayArgs.lockToDomNode = linkNode;
        
        // populate overlay with content
        var render = function() {
            var overlayContainerNode = $$('.cart-confirm-overlay-container')[0];
            if (!overlayContainerNode) return;

            // position overlay
            if (overlayArgs.lockToMouse) {
                self.positionToMouse(self.clickEvent, overlayContainerNode);
            } else if (overlayArgs.setPosition) {
                overlayArgs.setPosition(linkNode, overlayContainerNode);
            } 
                    
            // localize images
            var checkoutBtnNode = overlayContainerNode.select('.btn-checkout')[0];
            checkoutBtnNode.style.visibility = "hidden"; // hide until set up
            self.regionalizeImage(checkoutBtnNode);
            var continueBtnNode = overlayContainerNode.select('.btn-continue')[0];
            continueBtnNode.style.visibility = "hidden"; // hide until set up
            self.regionalizeImage(continueBtnNode);
            
            // render text content
            var contentContainerNode = $('cart-confirm-content');
            var prodNameNode = overlayContainerNode.select('.popover-product-name')[0];
            prodNameNode.update(skuData.prodName);
            var addedMessageNode = overlayContainerNode.select('.confirm-added-message')[0];
            addedMessageNode.update(self.text_addedMessageCheckout);
            if (skuData.shadeName) {
                var shadenameNode = overlayContainerNode.select('.popover-shadename')[0];
                shadenameNode.update("&nbsp;-&nbsp;" + skuData.shadeName);
            }
            var cartAddResponseData = cartAddResponse.getData();
            var fssMessage = "";
            var hasFssMessage = false;
            var fssMessageNode = overlayContainerNode.select(".cart-confirm-fss-message")[0];
            if (fssMessageNode && cartAddResponseData && cartAddResponseData.trans_data) {
                fssMessage = cartAddResponseData.trans_data.free_shipping_message;
                if (fssMessage && fssMessage.length > 1 && fssMessage != null) {
                    hasFssMessage = true;
                    fssMessageNode.innerHTML = fssMessage;
                    fssMessageNode.style.display = "block";
                }
            }
            if (!hasFssMessage) fssMessageNode.hide();

            // check for error message
            var hasErrors = cartAddResponse ? cartAddResponse.getError() : false;         
            // show error
            if (hasErrors) {
                var errorMessageNode = overlayContainerNode.select('.cart-confirm-error-message')[0];
                var errorContainerNode = overlayContainerNode.select('.cart-confirm-error')[0];
                var messages = cartAddResponse.getMessages();
                errorMessageNode.innerHTML = messages ? messages[0].text : ""; 
                errorContainerNode.removeClassName("hidden");
                contentContainerNode.addClassName("hidden");  
            } 
            
        };        

        var getTemplate = function() {
            generic.templatefactory.get({path: '/templates/products-cart-confirm.tmpl'}).evaluateCallback({
                callback: function(html) {
                    overlayArgs.content = html;
                    // open overlay
                    generic.overlay.launch(overlayArgs);                    
                    render();
                }
            }); 
        };
        
        getTemplate();
    
    },
    
    regionalizeImage: function(imgNode) {
        if (global.localeFileSuffix && global.localeFileSuffix !== "") {
            var src = imgNode.src;
            var base = src.split(".gif")[0];
            src = base + global.localeFileSuffix + ".gif";
            imgNode.src = src;
        }
        imgNode.style.visibility = "visible";
    },
    
    // position popup near mouse
    positionToMouse: function(evt, node) {
        if (!evt || !node) return;
        var lmin = 190;
        var lmax = 700;
        var tmin = 5;
        //var xOffset = 310; // left nav (190) & half of length of popup (120)
        var yOffset = -100; // ~ height of popup 
        var xOffset = 100;
        
        var t = (evt.pageY + yOffset);
        var l = (evt.pageX + xOffset); 
        // adjust for left & right edges
        if (l < lmin) {
            l = lmin;
        } else if (l > lmax) {
            l = lmax;
        }
        if (t < tmin) t = tmin;
        
        //console.log("evt.pageY = "+evt.pageY+" evt.pageX = "+evt.pageX+" / t = "+t+" l = "+l);
        node.style.top = t + "px";
        node.style.left = l + "px";           
    }

};

/**
 * brand.product.waitlist
 * basic handling for waitlist popovers/content where present
 * @memberOf brand.product
 */
brand.product.waitlist = {
    
    // note: currently very simple, but placeholder for waitlist instances that have more fancy behavior as some have had in the past
    init: function() {
        //console.log("brand.product.waitlist.init");
        var waitListOverlays = $$("div.overlay-container-waitlist");
        var count = waitListOverlays.length;
        var self = this;
        for (i=0; i<count; i++) {
            self.initOverlay({ overlayContainerNode: waitListOverlays[i] });
        }
    },
    
    initOverlay: function(args) {
        if (!args) return;
        var overlayContainerNode = args.overlayContainerNode;
        if (!overlayContainerNode) return;
        var closeLink = overlayContainerNode.select(".close-link")[0]; // look for a close link
        if (!closeLink) return;
        // attach event handler to close links
        closeLink.observe("click", function(clickEvt) {
            clickEvt.preventDefault();
            overlayContainerNode.hide();
        });
        
        /*
        // close overlay after N milliseconds
        var reset = function() {
            clearTimeout(durationTimer);
            overlayContainerNode.hide();
        }
        var durationTimer = setTimeout(reset, 5000);
        */
    }
};
// SPP functions
brand.spp = brand.spp || {};

// for non-shaded prods & shaded prods that are supposed to display like non-shaded
brand.spp.setSkuSelection = function(args) {
    var sku = args.sku;
    // inventory status
    site.product.inventoryStatus({
        shoppable: sku.shoppable,
        message: sku.inventory_status_message,
        messageNode: site.spp.inventoryStatusNode,
        containerNode: $("prod-details")
    });
   
    site.spp.skuField.value = sku.path;
    site.spp.skuFavField.value = sku.path;
    args.cartConfirmMsg.sku = sku;
    
    // shaded: product photo per-sku
    if (args.isShaded) {
        hasPhotosBySku = site.spp.photoBySku.init();
        if (hasPhotosBySku) {
            site.spp.photoBySku.onSkuSelect(sku, "0");
        }
    }
};

    
// swatch display for thumbnail grid & name list
brand.spp.initSwatches = function(args) {
    if (!args.node) { return; }
    var swatchSet;
    var swatchArgs = args.swatchArgs;
    swatchArgs.parentId = args.node;
    var prodBrowserSkuField = args.pageArgs.prodBrowserSkuField;
    var product = swatchArgs.product;
    var skuField = swatchArgs.skuField;
    var favField = args.pageArgs.favField;
    var filters = {};
    var cartConfirm = args.pageArgs.cartConfirm;
    swatchArgs.multiShaded = args.pageArgs.multiShaded; // multi-shaded prods that display separate swatch images for each color value
    var hasPhotosBySku = false;
    var swatchCardId = "swatchcards";
    var draggable;

    // swatch cards for all shades
    var swatchCard = new brand.product.swatchCard({
        id: swatchCardId,
        nodeToReplace: $("swatchcard-placeholder"),
        skus: product.skus,
        price: product.price,
        cartConfirm: cartConfirm,
        multiShaded: swatchArgs.multiShaded,
        closeOnClickOutside: { 
            enable: true,
            nodesToExclude: [ $(swatchArgs.domParent) ] // don't allow clicks in thumbs area to bubble up to body (at which point it would cancel out the opening of the swatch card)
        }
    });

    // single sku multi-shaded prod
    if (swatchArgs.multiShaded && swatchArgs.multiShaded.isSingleSkued) {
        //console.log("brand.spp.swatchSet.init: multiShaded single sku"); 
        // prods that display multi-colored sets of swatches & only display 1 sku per page        
        var sku = product.skus[0];
        
        // inventory status
        site.product.inventoryStatus({
            shoppable: sku.shoppable,
            message: sku.inventory_status_message,
            messageNode: site.spp.inventoryStatusNode,
            containerNode: $("prod-details")
        });
        
        // on swatch select
        swatchArgs.onSelectCallback = function(selectedChild, args) {
            //console.log("swatchArgs.onSelectCallBack selectedChild = ",selectedChild);
                        
            // open card on selection if not triggered by load              
            if (swatchCard && (!args || args.event !== "load")) {
                swatchCard.onSwatchSelect({ sku: sku, swatchNode: selectedChild.domNode, swatchIdx: selectedChild.idx });
            }

        }
        swatchArgs.initDefault = false; // don't need to set default, since skuField set explicitly for multi's
        swatchSet = new brand.product.thumbSwatchSet(swatchArgs, args.node);
        skuField.value = sku.path;
        favField.value = skuField.value;
        cartConfirm.sku = sku;
    
    // multi-skued prods
    } else {
        //console.log("brand.spp.swatchSet.init: multiple skus");
        var multiShaded = (swatchArgs.multiShaded.isMultiSkued ? true : false);

        // set up main image swapping by sku
        hasPhotosBySku = brand.spp.photoBySku.init();

        // on swatch select
        swatchArgs.onSelectCallback = function(selectedChild, args) {
            //console.log("swatchArgs.onSelectCallBack selectedChild = ",selectedChild);
            //console.log("swatchArgs.onSelectCallBack args = ",args);
            var sku, skuIdx, swatchNode;
            if (selectedChild && selectedChild.sku) {
                sku = selectedChild.sku;
                skuIdx = selectedChild.idx;
            } else if (args) {
                sku = args.sku;
                skuIdx = args.skuIdx;
            }
            var swatchIdx = (multiShaded && multiShaded.isMultiSkued && args.swatchIdx ? args.swatchIdx : selectedChild.idx);
            
            // if selection wasn't triggered from name menu or initial load open the card
            // Note: swatch card internally handles the setting of its cart button 
            // open card on selection if not triggered by load or name menu
            if (!args || args.event !== "name-menu") {              
                if (swatchCard && (!args || args.event !== "load")) { // can't do on load, since menu might not have been created
                    if (!multiShaded) brand.spp.swatchSorters.updateNameMenu(skuIdx); // multi-shaded, multi-skued prods don't update menu on swatch click, since visibility of current set of swatches is only set via name menu
                    if (!multiShaded || (multiShaded && args.event !== "subset-select")) swatchCard.onSwatchSelect({ sku: sku, swatchNode: selectedChild.domNode, swatchIdx: swatchIdx });
                }
            }

            // handle page/main inventory status node display
            // (swatch card status node handled by swatchCard class)
            site.product.inventoryStatus({
                shoppable: sku.shoppable,
                message: sku.inventory_status_message,
                messageNode: site.spp.inventoryStatusNode
            });
                
            // tell page favorites button, cart confirm & color play button which sku has been selected
            // Note: swatchSet class internally handles setting skuField
            // multi-shaded multi-skued prods would only set this on selection of whole sku set, rather than on selection of individual swatches
            if (!multiShaded || (multiShaded && args.event === "subset-select")) {
                if (favField) favField.value = sku.path;
                cartConfirm.sku = sku;
                if (prodBrowserSkuField) prodBrowserSkuField.value = selectedChild.hex;           
                // swap main Image
                if (hasPhotosBySku) brand.spp.photoBySku.onSkuSelect(sku, skuIdx);
            }
        }
        
        //console.log("brand.product.initSwatchSet calling SC: " ,swatchArgs); 
        swatchSet = new brand.product.thumbSwatchSet(swatchArgs, args.node);
        
        // init swatch sorting/filtering logic
        brand.spp.swatchSorters.init({
            swatchSet: swatchSet,
            product: product,
            shadedType: swatchArgs.shadedType, // shaded type set for all prods
            multiShaded: swatchArgs.multiShaded, // multiShaded params only set for true multiShade display types (single-skued and multi-skued with multiple swatches per sku)
            hasTabs: args.pageArgs.hasTabs
        });
        
        // now that both swatches & menus have initialized, update name menu
        // to reflected the sku selection made at initial load time
        var selectedIndex;
        if (swatchSet.selectedChildWidget && typeof swatchSet.selectedChildWidget.idx !== "undefined") {
            selectedIndex = swatchSet.selectedChildWidget.idx;
        } else if (multiShaded && swatchSet.selectedChildNode && typeof swatchSet.selectedChildNode.idx !== "undefined") {            
            selectedIndex = swatchSet.selectedChildNode.idx;
        }
        if (selectedIndex >= 0) brand.spp.swatchSorters.updateNameMenu(selectedIndex);
    }
    
    // make swatch card container available on multi-shaded spp
    if (swatchArgs.multiShaded) $("swatch-pop-container").removeClassName("hidden");
    draggable = new Draggable(swatchCardId);
   
    return swatchSet;
};

// sorting/filtering drop-downs
brand.spp.swatchSorters = {

    _currentFilter: "all", // "all" = showing all shades
    _savedFilterStates: {},
    _settingFilter: false,
    
    finishesKey: "finishes",
    nameKey: "name",
    searchKey: "search",
    
    init: function(args) {
        //console.log("brand.spp.swatchSorters");  
       
        var swatchSet = args.swatchSet;
        var product = args.product;
        this.filters = {}; // filter hash to pass to swatchSet
        var finishFilters = {};
        this.multiShaded = args.multiShaded;

        // dom nodes
        var nameMenuNode = $("menu-swatches-byname");
        this.nameMenuContainer = $("menu-swatches-byname-container");
        this.finishMenuContainer = $("menu-filter-byfinish");
        var searchNodes = {
            container: $("product-search"),
            fieldId: "product-search-field",
            button: $("product-search-button"),
            message: $("product-search-message")
        }
        
        this.nameMenu = this._initNameMenu(swatchSet, product, nameMenuNode);
        this.searchForm = this._initSearch(swatchSet, product, searchNodes);
        // get/set filters
        if (args.shadedType !== "duo") {
            // by finish
            finishFilters = this._getFinishFilters(product);
            if (finishFilters) {
                this.filters = finishFilters.filters; // save each finish is a sibling of other filters (i.e. "matte" & "limited")
                this.finishMenu = this._initFinishFiltering(swatchSet, finishFilters);
            }
        }

        // get best sellers data or pro products data but not both
        var showProProducts = (global.ispro ? true : false);
        var otherFilters = ["limited_life"]; // page data keys
        if (showProProducts) {
            otherFilters.push("pro_products"); // page data key
        } else if (product.has_top_sellers == 1) {
            // top sellers: reformat data into common filters format
            var topsellerFilter = this._getFilterBySkuId({ product: product, skuArray: product["top_seller_skus"] });
            // page data may say "has_top_sellers == 1", but top_seller skus listed may not match any that are displayable on page
            if (topsellerFilter && topsellerFilter.length > 0) this.filters["top_sellers"] = topsellerFilter;
        }
        
        // get/set one-dimensional filter data (e.g. not top sellers or finishes)
        this._getFilters(product, otherFilters);

        // set filter property for swatchSet instance
        swatchSet.filters = this.filters;
        
        // tab changes trigger changes in swatch display/sorting
        if (args.hasTabs && brand.spp.tabContainer) {
            this._enableTabs({ swatchSet: swatchSet });
        }
    },

    _initNameMenu: function(swatchSet, product, menu) { 
        // set up callback for sorting menu
        if (!menu) { return false; }
        
        var self = this;        
        var names = this._getNames(product);
        var optionCount = menu.options.length; // if there's a default label we want to add to it rather than starting options at 0
        this._savedFilterStates[this.nameKey] = "all";
        var count = 0;
        
        // if only 1 name, hide menu and container
        if (names.length <= 1) {
            if (this.nameMenuContainer) {
                this.nameMenuContainer.hide();
            } else {
                menu.hide();
            }
            return false;
        }
        
        names.each(function(opt, idx) {
            count = (optionCount + idx);
            menu.options[count] = new Option(opt.label, opt.num); 
        });

        var onChange = function() {  
            //console.log("brand.spp.swatchSorters._initNameMenu onChange ");
            var value = this.getValue();
            if (typeof(value) === "undefined") { return; }
            if (self.multiShaded && self.multiShaded.isMultiSkued) {
                var sku = product.skus[value];
                swatchSet.setSwatchSubset({ sku: sku, event: "name-menu", skuIdx: value });
            } else {
                var swatch = $("swatch_" + product.skus[value].sku_id).widget;
                if (!swatch || swatchSet.selectedChildWidget == swatch) { return; }
                swatchSet.setSwatch(swatch, { event: "name-menu" });
                // if swatch seleted is not in current filter, set filter back to "all"
                var filter = self._currentFilter;
                if (filter !== "all") {
                    var hasSwatchInFilter = self.filters[filter].any(function(swatchIdx) { 
                        return (swatchIdx === value);
                    }); 
                    
                    // reset filter display state to "all"
                    if (!hasSwatchInFilter) {
                        self._setFilter(swatchSet, { value: "all", type: self.nameKey, event: "name-menu" });   
                    }
                }
            }
        }
        menu.observe("change", onChange);
        return menu;
    },
        
    _initFinishFiltering: function(swatchSet, finishFilters) {
        //console.log("_initFinishFiltering: "+menu);
        // populate filtering menu 
        if (!this.finishMenuContainer || !finishFilters) return false;
        
        var self = this;
        var labels = finishFilters.labels;
        var filters = finishFilters.filters;
        var count = 0;
        var type = this.finishesKey;
        var menu = this.finishMenuContainer.select("select")[0];
        var optionCount = menu.length;
        if (!menu) return false;

        for (var filter in filters) {
            menu.options[optionCount] = new Option(labels[filter], filter);
            optionCount++;
            count++;
        }
        
        // hide menu if no prod has no finishes
        if (count == 0) {
            this.finishMenuContainer.hide();
            return false;
        }
        
        // set up callback
        var onChange = function() {
            var value = this.getValue();
            //console.log("brand.spp.swatchSorters._initFinishFiltering onChange value = "+value+" self._currentFilter = "+self._currentFilter+" self._settingFilter = "+self._settingFilter);
            if (value !== self._currentFilter && !self._settingFilter) {
                self._setFilter(swatchSet, { value: value, type: type });
                swatchSet.domNode.show();
                self._savedFilterStates[type] = value; // save state
            }
        }
        menu.observe("change", onChange);
        
        return menu;
    },

    _initSearch: function(swatchSet, product, searchNodes) {
        if (!searchNodes || !searchNodes.container) return false;
        var self = this;
        var msgNode = searchNodes.message;
        msgNode.hide();
        var query = "";
        var isSearching = false;
        var progress = new brand.progress({
            containerNode: swatchSet.domNode,
            progressNode: searchNodes.container.select('.progress')[0]
        }); 
        
        var showSkus = function(response) {
            var data = response;        
            
            if (!data || !data.skus || data.skus.length < 1) {
                // empty results
                onNoResult(response);
                
            } else {
                var resultCount = data.skus.length;
                isSearching = false;
                // set filter of sku indices
                self.filters[self.searchKey] = self._getFilterBySkuId({ product: product, skuArray: data.skus });
                self._setFilter(swatchSet, { value: self.searchKey, type: self.searchKey, nocache: true });
                progress.clear();
                var resultMessage = site.product.rb.search_results.replace("RESULTS", resultCount);
                msgNode.update(resultMessage);
                msgNode.show();
            } 
        }

        var onNoResult = function(response) {
            console.log('RPC call successful, but no results returned. response = ',response);
            var resultMessage = site.product.rb.search_no_results;
            onMessage({ message: resultMessage });
        }
        
        var onFailure = function(response) {
            var message = response.getMessages();
            console.log('Product Search JSON failed to load. response = ',message);
            onMessage({ message: "Error: "+message[0].text });
        }

        var onMessage = function(args) {
            isSearching = false;
            progress.showMessage({ messageNode: msgNode, message: args.message, hideContainer: true });
            self.filters[self.searchKey] = null;
        }
        
        var getSkus = function() {
            if (isSearching) return;
            isSearching = true;
            progress.start();
            query = $F(searchNodes.fieldId);

            var endecaArgs = {
                searchTerm: query,
                filterProducts: true,
                recsPerPage: 500,
                additionalFilters: 'p_PRODUCT_ID:' + product.product_id,
                searchKey: 'shade'
            };
        
        
            var shadeQuery = new EndecaQuery (
                Object.extend( endecaArgs, {
                    callbackCompleted: function() {
                        var eCatalog = new EndecaCatalog({jsonResult: this.jsonResult});
                        showSkus(eCatalog.productList[product.product_id]);
                    }       
                })
            );
            
            shadeQuery.makeRequest();

        }
        searchNodes.button.observe("click", getSkus);
        
        return searchNodes.container;
    },
    
    updateNameMenu: function(idx) {
        //console.log("brand.product.updateNameMenu "+idx+" this.nameMenu = "+this.nameMenu); 
        if (this.nameMenu) {
            this.nameMenu.value = idx;
        }
    },
    
    _getNames: function(product) {
        var names = [];
        var sortedSkus = product.sorters.names;
        var count = sortedSkus.length;
        for (var i=0; i<count; i++) {
            var name = {};
            var num = sortedSkus[i];
            name.num = num.toString();
            name.label = product.skus[num].shade_name;
            names[i] = name;
        }
        return names;            
    },

    _setFilter: function(swatchSet, args) {
        var prev = this._currentFilter;
        var value = args.value;
        this._currentFilter = value;
        if ((prev === this._currentFilter && !args.nocache) || this._settingFilter) { return; }
        this._settingFilter = true;
        
        // send sort/filter value to swatchSet instance
        //console.log("swatchSet filters = ",swatchSet.filters);
        swatchSet.processData("filter", value, args.nocache);
        //console.log("swatchSet.processData: _setFilter - to: "+value+" type = "+args.type);

        // name menu selection is forcing a re-display of all swatches so show its tab
        if (args.event === "name-menu" && value === "all") {
            brand.spp.tabContainer.setTabRemotely({ sortingKey: args.type });
        }
        
        this._settingFilter = false;
    },
    
    _getFinishFilters: function(product) {
        var filters = {};
        var labels = {};
        var skusByFinish = product.filters[this.finishesKey];
        var sortedFinishNames = product.filters.sorted_finishes;
        hasFinishes = false;
        if (!sortedFinishNames) return false;
        var finishCount = sortedFinishNames.length;  

        for (i=0; i<finishCount; i++) {
            var finishName = sortedFinishNames[i]; // finishId = id value for menu
            // convert indices to strings
            var skus = skusByFinish[finishName].toString();
            skus = skus.split(",");
            hasFinishes = true;
            var finishId = finishName.replace(/ /, ""); // finishId = id value for menu (lowercased, white-space, removeed, etc...)
            finishId = finishId.toLowerCase();
            var label = finishName; // label = display label for menu (entities stripped)
            // replace html entities
            var entitiesToUnicode = site.spp.entitiesToUnicode;
            if (entitiesToUnicode && (label.indexOf("&") > -1)) {
                for (var entity in entitiesToUnicode) {
                    var entityString = new RegExp(entity, 'g');
                    label = label.replace(entityString, entitiesToUnicode[entity]);
                }
            }
            
            labels[finishId] = label;
            filters[finishId] = skus;  
        } 

        if (!hasFinishes) return false;
        return { filters: filters, labels: labels };
    },

    // get skus when data is a set of sku_ids instead of a list of sku indices
    _getFilterBySkuId: function(args) {
        var filter = [];
        if (!args.product || !args.skuArray) return false;
        var product = args.product;
        var skuArray = args.skuArray;
        this.skuIndices = (this.skuIndices ? this.skuIndices : this._getSkuIndices(product.skus));
        if (!skuArray || skuArray.length == 0) return false;
        var resultIdx = i = 0;
        var count = skuArray.length;
        var arrayValues = {}; // temporary object to store values of array so loop can clean out duplicates
        
        // loop thru skus to find index of top seller sku ids
        for (i=0; i<count; i++) {
            var skuId = skuArray[i];
            // json-rpc results return obj as: {SKU_ID="SKU1688"}
            if (typeof skuId === "object") {
                if (skuId.SKU_ID) skuId = skuId.SKU_ID;
            }
            if (arrayValues[skuId]) {
                skuArray.splice(i,1);
                i--; count--;
                continue;
            }
            arrayValues[skuId] = i;
            var skuIdx = this.skuIndices[skuId];
            if (!skuIdx) {
                skuArray.splice(i,1);
                i--; count--;
                continue;
            }
            filter[resultIdx] = skuIdx;
            resultIdx++;
        }
        return filter;             
    },
    
    _getFilters: function(product, dataKeys) {
        if (!product.filters) return;
        var self = this;
        
        dataKeys.each(function(key){
            var filterIndices = product.filters[key];
            var count = filterIndices.length;
            if (filterIndices && count > 0) {
                // convert int indices into strings
                var filter = [];
                for (var i=0; i<count; i++) { 
                    var num = filterIndices[i];
                    filter.push(num.toString());
                }
                
                // add filter to master filter hash
                if (filter.length > 0) {
                    self.filters[key] = filter;
                }
            }
        });             
    },
    
    // store position of skus in sku array, keyed by sku ids
    _getSkuIndices: function(skus) {
        if (!skus) return false;
        var indices = {};
        var skuCount = skus.length;
        // loop thru skus to find index       
        for (i=0; i<skuCount; i++) {
            var skuId = skus[i].sku_id;
            indices[skuId] = i.toString(); // convert int indices into strings
        }
        return indices;    
    },

    _enableTabs: function(args) {
        if (!brand.spp.tabContainer.tabs) return;
        var self = this;
        generic.events.observe("tabs:beforeshow", function(eventArgs) {
            self.onTabChange(args.swatchSet, eventArgs);
        });

        // display tabs with sorting/filtering data
        var sortingKeys  = brand.spp.tabContainer.sortingKeys;
        for (var key in sortingKeys) {
            var tabId = sortingKeys[key];
            var tab = $(tabId);
            if (!tab) continue;
            var parent = tab.parentNode;
            if (!parent) continue;
            
            if (key === this.finishesKey && this.finishMenu) {
                // finishes tab
                parent.removeClassName("hidden");
            } else if (this.filters[key]) {
                // other tabs
                parent.removeClassName("hidden");
            }
        }
        
        // reset scrolling state now that content has loaded
        brand.spp.tabContainer.tabs.resetScrolling();
    },
    
    onTabChange: function(swatchSet, args) {
        //console.log("onTabChange args = ",args);
        //console.log("onTabChange, this._savedFilterStates = ",this._savedFilterStates);
        var selectedLink = args.selectedLink;
        if (!selectedLink) return;
        var selectedSort = (args.selectedSort ? args.selectedSort : this.nameKey);
        var activeSort = (args.activeSort ? args.activeSort : this.nameKey);
        var finishMenu = this.finishMenu;
        var searchForm = this.searchForm;
        var sortValue = (this._savedFilterStates[selectedSort] ? this._savedFilterStates[selectedSort] : selectedSort);

        // toggle display of swatches, filter menu & search form when tabbing away from their tab
        if (finishMenu && activeSort === this.finishesKey) {
            this.finishMenuContainer.hide();
            swatchSet.domNode.show();
        } else if (searchForm && activeSort === this.searchKey) {
            searchForm.hide();
            swatchSet.domNode.show();           
        }
            
        // case: filter by finish        
        if (finishMenu && selectedSort === this.finishesKey) {
            // toggle display of filter menu
            this.finishMenuContainer.style.display = "block";
            if (finishMenu.selectedIndex == 0) {
                swatchSet.domNode.hide();
            } else {
                // show the state of the finish filter with last known selected finish
                this._setFilter(swatchSet, { value: sortValue, type: selectedSort });
            }

        // case: search results
        } else if (searchForm && selectedSort === this.searchKey) {
            // toggle display of search form
            searchForm.style.display = "block";
            // show swatches if there's a previous result set to show
            var searchResults = this.filters[this.searchKey];
            if (searchResults && searchResults.length > 0) {
                this._setFilter(swatchSet, { value: sortValue, type: selectedSort, nocache: true });
            } else {
                swatchSet.domNode.hide();
            }
          
        // case: other tabs
        } else {
            
            // set display of swatches based on selected tab
            this._setFilter(swatchSet, { value: sortValue, type: selectedSort });
        }
    
    }
};


// spp tab handler
brand.spp.tabContainer = {

    // tab names as paired with swatch sorting types 
    sortingKeys: {
        "name": "tab-shades",
        "pro_products": "tab-pro",
        "limited_life": "tab-limited",
        "top_sellers": "tab-bestsellers",
        "finishes": "tab-byfinish",
        "search" : "tab-search"
    },
    
    init: function() {
        var self = this;
        this.tabs = new brand.tabs("prod-tabs-nav",
            {
                tabContainer: $("prod-tabs"),
                activeClassName: "tab-active",
                beforeShow: function(link) {
                    self.beforeShow(link);
                },              
                useImageHeaders: true,
                scrollbar: {
                    contentNode: $('scroll-content-container'),
                    containerNode: $("scrollbar-container"),
                    handleId: "scrollbar-handle",
                    trackId: "scrollbar-track",
                    enabledClass: "scrollbar-enabled"  
                }
            }
        );
        
        // save sorting types by tab
        var tabKeys = {};
        for (var key in this.sortingKeys) {
            var value = this.sortingKeys[key];
            tabKeys[value] = key;
        }
        this.tabKeys = tabKeys;
    },
    
    beforeShow: function(link) {
        var activeLink = (this.tabs ? this.tabs.activeLink : null);
        if (!link || !activeLink) return;
        if (link.id === this.tabs.activeLink.id) return;
        
        // broadcast tab change for swatch sorting based on tab
        generic.events.fire({event: "tabs:beforeshow", msg: { selectedLink: link, selectedSort: this.tabKeys[link.id], activeSort: this.tabKeys[activeLink.id] } });    
    },
    
    setTabRemotely: function(args) {
        var sortingKey = args.sortingKey;
        var linkId = args.linkId;
        if (!sortingKey && !linkId) return;
        var link = (sortingKey ? $(this.sortingKeys[sortingKey]) : $(linkId));
        if (!link || (link.id === this.tabs.activeLink.id)) return;
        this.tabs.setActiveTab(link);
    }
    
};

// "more" description
brand.spp.initDescription = function(args) {
    var linkNode = args.linkNode;
    var descriptionNode = args.descriptionNode;
    if (!linkNode || !descriptionNode || !args.hasDescription) return;
    
    linkNode.removeClassName("hidden");
    
    // event handlers
    linkNode.observe("mouseover", function() {
        descriptionNode.style.visibility = "visible";
        brand.spp.toggleFormSelectForIE6(false);
    });
    descriptionNode.observe("mouseout", function() {
        descriptionNode.style.visibility = "hidden";
        brand.spp.toggleFormSelectForIE6(true);
    });
};


// color play link
brand.spp.initColorPlayButton = function(field) {        
    var url = "/flash/color_play/index.tmpl";
    var param = "?colorplaysample=";
    if (!field) return false;
        
    var _onClick = function(e) {
        var hex;
        hex = field.value.split("#")[1];
        if(hex) {
            hex = "0x" + hex;
            location.href = url + param + hex;
        } else {
            location.href = url;
        }
    }
    field.observe("click", _onClick);
}

// main image rollover
brand.spp.photoRollover = {
    hasRollover: false,
    overImg: null,
    outImg: null,
    init: function(outImg, overImg) {
        var node = $("prod-image");
        if (!node) {
            return;
        }
        // if first set of image paths is passed then set to true
        // otherwise will start off as false, but can be set to true if brand.spp.photoBySku
        // passes valid per-sku image paths on sku selection
        if (outImg && overImg) {
            this.hasRollover = true;
            this.outImg = outImg;
            //this.overImg = overImg;
            this.overImg = this.defaultOverImg = overImg; // Note: assumes that brand wants a single sku's over/_alt image to act as the default overImg for any skus that don't have one
        } else {
            return; // SS NOTE: for now ignore if no default product.image_medium_rollover since many skus with sku.image_medium_rollover seem to be 404
        }

        var self = this;
        
        var over = function(e) {
            if (!self.hasRollover) return;
            e.target.src = self.overImg;
        }
        
        var out = function(e) {
            if (!self.hasRollover) return;
            e.target.src = self.outImg;
        }
        node.observe("mouseover", over);
        node.observe("mouseout", out);
    }
};

// main image swap on shade selection
brand.spp.photoBySku = {
    init: function() {
        this.node = $("prod-image");
        if (!this.node) return false;
        this.preloaded = {};
        return true;
    },
    
    onSkuSelect: function(sku, swatchIdx) {
        //console.log("brand.spp.photoBySku = "+swatchIdx);
        var img = sku.sku_image; // sku-specific image that shows on swatch click
        if (!img) return;
        var overImg = (sku.image_medium_rollover ? sku.image_medium_rollover : null); // sku-specific "on" state of outImg
        
        // preloads image (of not already pre-loaeded) & sets new src
        var preloaded = brand.loadImage({
            node: this.node,
            imagePath: img,
            imageStore: this.preloaded,
            imgId: swatchIdx
        });
        this.preloaded = preloaded;
  
        // if photo rollover, set on & off state to currently selected "special" image
        var pr = brand.spp.photoRollover;
        if (pr.hasRollover) {
            pr.outImg = img;
            if (overImg) {
                pr.overImg = overImg;
            } else {
                pr.overImg = pr.defaultOverImg;
            }
        }
        /* SS note: use this if sku.image_medium_rollover is meant to be reliable
        if (overImg) {
            pr.hasRollover = true;
            pr.outImg = img;
            pr.overImg = overImg;
        } else {
            pr.hasRollover = false;
        }
        */
    } 
};

// products with multiple skus based on size (non-shaded)
brand.spp.initSized = function(args) {
    var menu = args.menuNode;
    var skus = args.skus;
    var skuCount = skus.length;
    if (skuCount < 1) return;
    var firstSkuId = 0;

    // populate menu
    if (!args.menuNode) {
        site.spp.setSkuSelection({ sku: skus[0], cartConfirmMsg: args.cartConfirmMsg });
        return; // if returning here, then this method was somehow applied to a single-skued product in which case we still want to do sku selection
    }

    var skuIndices = {};
    var skuIdArray = new Array();
    for (var i=0; i<skuCount; i++) {
        var skuId = skus[i].sku_base_id;
        skuIdArray.push(skuId);
        skuIndices[skuId] = i; // keep track of sku index in data
    }
    var sizeNode = $$("div#prod-container span.size")[0];
    var priceNode = $$("div#prod-container span.price")[0];

    // Sort by sku_base_id to get Small, Medium, Large order
    skuIdArray = skuIdArray.sort();
    for (var i=0; i<skuCount; i++) {
        var skuId = skuIdArray[i];
        var skuIndex = skuIndices[skuId];
        var sizeInfo = skus[skuIndex].product_size;
        var label = (sizeInfo ? sizeInfo : "");
        menu.options[i] = new Option(label, skuIndex);
        if (i == 0) {
            firstSkuIdx = skuIndex;
        }
    }

    var setSkuSelection = function(skuIdx) {
        var sku = skus[skuIdx];
        //console.log("sized sku selection = "+sku.sku_base_id+" / "+sku.product_size);
        site.spp.setSkuSelection({ sku: sku, cartConfirmMsg: args.cartConfirmMsg });
        if (sizeNode) sizeNode.innerHTML = sku.product_size;
        if (priceNode) priceNode.innerHTML = sku.formatted_price;
    }
    // pre-select first sku
    setSkuSelection(firstSkuIdx, "size");
    
    // menu event callback
    var onChange = function() {  
        //console.log("brand.spp sized onChange ");
        var value = this.getValue();
        //console.log("brand.spp sized onChange value = "+value);
        if (typeof(value) === "undefined") { return; }
        // set selected sku
        setSkuSelection(value);
    }
    menu.observe("change", onChange);
};

// IE6 workaround: toggle display of form select(s) when popovers are showing
brand.spp.toggleFormSelectForIE6 = function(show) {
    if (generic.env.isIE6) {
        var container = $("prod-container");
        if (container) {
            if (show) {
                container.removeClassName("popup-visible");
            } else {
                container.addClassName("popup-visible");
            }
        }
    }
};
brand.product.swatchSet = Class.create(Widget, {    
    sortType: "",    
    isContainer: true,
    isActive: false,
    _started: false,
    _loaded: null,
    _dataMethod: "sort",
    _dataParam: "",
    _activeSet: "",
    _initialized: 0,
    
    // _swatchSelected: Boolean
    // flag true when a sku has been selected
    _swatchSelected: false,

    // initDefault: Boolean
    // start with first swatch selected by default
    initDefault: false,
    
    shadedType: "solo",
    isDiscontinued: false,
    selectedSku: "",

    // productType: String (optional)
    // additional identifier to distinguish btwn products appearing more than once on page.
    // ex: cross-sell vs. main product
    productType: "",
    
    skus: "",
    smooshImg: "",
    video_prod: false,

    sorters: {},
    filters: {},
    
    initialize: function($super, arguments, node) {
        this.setProperties(arguments);
        this.skus = this.product.skus;
        if (this.product.video_prod) this.video_prod = true;
        // clear out & reference as subset instead
        // some extra attributes added here for external use (e.g. shop together)
        this.productData = {
            id: this.product.product_id,
            sorters: this.product.sorters,
            name: this.product.name,
            image_small: this.product.image_small,
            uri: this.product.uri
        }
        this.product = {};
        this.id = node.id; 
        var self = this;
        this._loaded = {};
 
        this.sortType = (this.sortType !== "" ? this.sortType : "color");
        this._dataParam = this.sortType;
        //console.log(productData.name + " has productType = "+this.productType);
        
        if (this.multiShaded && this.multiShaded.isSingleSkued) {
            //console.log("brand.product.swatches.initialize: this.multiShaded.isSingleSkued");
            var sku = this.skus[0];
        
            // check for multiple values (arrays) in single sku
            if (Object.isArray(sku.smoosh) && sku.smoosh.length > 0) {
                this.smooshes = {};        
            }
            // save shade info (alternate to sku.smoosh, etc...)
            var idbase = this.productData.id + "_" + this.productType;
            //console.log("this.multiShaded.isSingleSkued " + this.productData.name + " idbase = "+idbase);
            for (var i = 0; i < sku.color.length; i++) {
                var id = "swatch_" + idbase + i.toString();
                if (this.smooshes) {
                    this.smooshes[id] = sku.smoosh[i];        
                }          
            }            
        } else {        
            // template has a sort order
            // multiShaded.isMultiSkued we're not using sorters
            if (this.productData.sorters && !this.multiShaded) {
                this.sorters = this.productData.sorters;
            } else {
                // creates default sort order from order of skus in page_data
                var set = []; 
                this.skus.each(function(sku, idx) { 
                    set[idx] = [idx];
                });
                this.sorters[this._dataParam] = set;
            }
        } 
         
        $super();
    },
    
    setSwatch: function(child, args) { 
        //console.log("setSwatch: "+child.id+" / child = ",child);
        //console.log("setSwatch: child idx ",child.idx);
        //if (args && args.event !== "load") { // on user click event
        if (child && child.sku) { // send event even on initial load
           // fire event broadcaster for external use (e.g coremetrics or shop together)
           document.fire("swatch:click", { sku: child.sku, shadeIdx: child.idx, product: this.productData });
        }
        if (!child || this.selectedChildWidget == child) { 
            this.onSelectCallback(child, args);
            return;
        }
        var sku = child.sku;
        
        // toggle state of previous & current selected swatches
        if (this.selectedChildWidget) {
            this.selectedChildWidget.selected = false;
            this.toggleSelectedState(this.selectedChildWidget, false);
        }
        child.selected = true;
        this.toggleSelectedState(child, true);
        this.selectedChildWidget = child;
        if ((!this.multiShaded || (this.multiShaded && !this.multiShaded.isSingleSkued)) && this.skuField) {
            this.skuField.value = child.sku.path;
        }
        this.onSelectCallback(child, args);
        this._swatchSelected = true;
    },
    
    onSelectCallback: function(child, args) {
        // for external callback use
    },

    processData: function(method, param, nocache) {
        //console.log("brand.product.swatchSet.processData");
        if (!nocache && this.started && (this._dataMethod === method && this._dataParam === param)) { return; }
        this._dataMethod = method;
        this._dataParam = param;

        var set;
        if (method === "sort") {
            if (param === "status") {
                var sorters = this.sorters[param];
                for (var i = 1; i <= 4; i++) {
                    var sub = sorters[i.toString()];
                    if (!set) {
                        set = sub;
                    } else {
                        set.concat(sub);
                    }
                }
            } else {
                set = this.sorters[param];
            }
        } else {
            if (param === "all") {
                this._dataMethod = "sort";
                set = this.sorters[this.sortType];
            } else {
                var filters = this.filters;
                if (filters[param]) {
                    set = filters[param];
                }
            }
        }
        
        this._activeSet = set;
        this._updateSet();
    },

    _updateSet: function() { 
        var children = this.children;
        if (children && this._initialized > 0) {
            var childrenCount = children.length;
            for (var i = 0; i < childrenCount; ++i) {
                var child = children[i];
                $(child.id).style.display = "none";
            }
        }
        
        this._initialized++;
        var ids = this._activeSet;
        //console.log("_updateSet "+ids.length+" this._started = "+this._started);
        if (!ids) return;
        
        var idsCount = ids.length;
        for (var i = 0; i < idsCount; ++i) {
            var idx = ids[i];
            var swatch = this._loaded[idx];
            //console.log("add/show swatch: "+idx+" / "+swatch.sku.shade_name);
            if (!swatch) {
                console.log("idx = "+idx+" ids = ",ids);
                continue;
            }
            var swatchNode = $(swatch.id);
            if (!swatchNode) continue;
            swatchNode.style.display = "block";
            this.domNode.appendChild(swatchNode);
            if (this._addClassByColumn) {
                this._addClassByColumn(swatch, i);
            }
            
            // display with specified starting swatch, if any
            if (!this._started) {
                if (this.selectedSku && (this.selectedSku === swatch.sku.sku_id )) {
                    this.setSwatch(swatch, { event: "load" });
                }
            }
        }
              
    }
    
});


brand.product.hexSwatchSet = Class.create(brand.product.swatchSet, 
{
    templateKey : "hexSwatchSet",
    templateString: '<div id="#{id}" class="swatchset-hex-container"></div>',
    
    // skuField: DOM node
    // input field for storing selected sku
    skuField: null,
    
    selectedClass: "swatch_hex_container_selected",
    selectedHexClass: "swatch_hex_selected", 
    
    postCreate: function() {
        //console.log("brand.product.hexSwatchSet.postCreate "+this.id); 
        var skuCount = this.skus.length;
        for (var i = 0; i < skuCount; ++i) { 
            var sku = this.skus[i];
            var cid = "swatch_" + sku.sku_id; 
            if (this.video_prod) cid = "video_" + cid;
            //console.log("brand.product.hexSwatchSet.postCreate, this.video_prod = "+this.video_prod+" cid = "+cid);
            
            var swatch = new brand.product.hexSwatch({
                id: cid,
                sku: sku,
                containerId: this.id,
                parentId: this.id,
                idx: i
            });                
            this._loaded[i] = swatch;
            
        }
        
        this.processData(this._dataMethod, this._dataParam);
        this._started = true;
    },
    
    toggleSelectedState: function(child, state) {
        if (!child || !this.selectedClass) return;
        if (state) {
            child.domNode.addClassName(this.selectedClass);
            child.hexNode.addClassName(this.selectedHexClass);
        } else {
            child.domNode.removeClassName(this.selectedClass);
            child.hexNode.removeClassName(this.selectedHexClass);
        }
    }
    
});


brand.product.thumbSwatchSet = Class.create(brand.product.swatchSet, 
{

    templateKey : "thumbSwatchSet",
    templateString: '<div id="#{id}" class="swatchset-thumbs-container"></div>',

    selectedClass: "swatch-thumb-selected",
    subsetClass: "swatchsubset-thumbs-container",
    selectedSubsetClass: "swatchsubset-selected",
    
    // skuField: DOM node
    // input field for storing selected sku
    skuField: null,

    // arrays for multiple values associated w/ each sku (ex: multi-colored skus)
    smooshes: null,
    
    // # of thumbs on each horizontal line
    columns: 7,
                        
    postCreate: function() {
        //console.log("brand.product.thumbSwatchSet.postCreate "+this.id); 
        var idbase = this.productData.id + "_" + this.productType;
        //console.log("brand.product.thumbSwatchSet.postCreate idbase = "+idbase);
        
        // multi-shaded, 1 sku per SPP
        if (this.multiShaded.isSingleSkued) {
            var sku = this.skus[0];
            var itemCount = sku.shade_name.length;
            for (var i = 0; i < itemCount; i++) { 
                var cid = "swatch_" + idbase + i.toString();
                var name = (sku.shade_name[i] ? sku.shade_name[i] : "");
                var hex = (sku.color[i] ? sku.color[i].toString() : null);
                var swatch = new brand.product.thumbSwatch({
                    id: cid,
                    sku: sku,
                    idx: i,
                    name: name,
                    smooshThumb: sku.smoosh_thumb[i],
                    hex: hex,
                    parentId: this.id,
                    parentIsMultiShaded: this.multiShaded
                });
                if (this._addClassByColumn) {
                    this._addClassByColumn(swatch, i);
                }
            }

        // multiple swatches per sku, multiple skus per SPP
        } else if (this.multiShaded.isMultiSkued) {       
            var skuCount = this.skus.length;
            var subsetNode;
            for (var i = 0; i < skuCount; ++i) { 
                var sku = this.skus[i];
                var itemCount = sku.color.length; // count swatches per sku by color array
                
                // create subset container for multi-shued swatch sets that need to display separately
                if (this.multiShaded.isMultiSkued) {
                    subsetNode = document.createElement("div");
                    subsetNode.id = "swatchsubset_" + sku.sku_id;
                    this.containerNode.appendChild(subsetNode);
                    $(subsetNode.id).addClassName(this.subsetClass);
                }

                for (var j = 0; j < itemCount; j++) {
                    var cid = "swatch_" + sku.sku_id + "_" + j.toString();
                    var hex = (sku.color[j] ? sku.color[j].toString() : null);
                    var swatch = new brand.product.thumbSwatch({
                        id: cid,
                        sku: sku,
                        //containerId: subsetNode.id,
                        idx: j,
                        smooshThumb: sku.smoosh_thumb[j],
                        hex: hex,
                        domParent: subsetNode,
                        parentId: this.id,
                        parentIsMultiShaded: this.multiShaded
                    });                
                }                
                // display with specified starting swatchsubset, if any
                if (this.selectedSku && (this.selectedSku === sku.sku_id)) {
                    this.setSwatchSubset({ sku: sku, event: "load", skuIdx: i });
                }
            }

            // set first sku as selected if none selected yet
            if (this.initDefault && !this._swatchSelected) {
                this.setSwatchSubset({ sku: this.skus[0], event: "load", skuIdx: 0 });  // set first sku as selected
            }
                
        // default shaded prod: 1 swatch per sku, multiple skus per SPP
        } else {        
            var skuCount = this.skus.length;
            for (var i = 0; i < skuCount; ++i) { 
                var sku = this.skus[i];
                var cid = "swatch_" + sku.sku_id;           
                var swatch = new brand.product.thumbSwatch({
                    id: cid,
                    sku: sku,
                    containerId: this.id,
                    idx: i,
                    parentId: this.id
                });                
                this._loaded[i] = swatch;
    
            }
            this.processData(this._dataMethod, this._dataParam);
            
            // set first sku as selected if this.selectedSku wasn't already selected in _updateSet method
            if (this.initDefault && !this._swatchSelected && this._loaded[0]) {
                this.setSwatch(this._loaded[0], { event: "load" });
            }
        }
        
        this._started = true;
    },
    
    setSwatchSubset: function(args) {
        var sku = args.sku;
        if (!sku) return;
        var subsetNode = $("swatchsubset_"+sku.sku_id);
        var skuIdx = args.skuIdx; // sku position in sku array (not shade idx within sku)
        //console.log("setSwatchSubset: args.skuIdx = "+args.skuIdx+" id: "+subsetNode.id+" / subsetNode = ",subsetNode);
        if (subsetNode && sku) { // send event even on initial load
           document.fire("swatch:click", { sku: sku, shadeIdx: 0, product: this.productData });
        }
        
        // toggle state of previous & current selected subsets
        if (this.selectedChildNode) {
            this.selectedChildNode.selected = false;
            this.selectedChildNode.hide();
        }
        subsetNode.style.display = "block";
        this.selectedChildNode = subsetNode;
        this.selectedChildNode.idx = skuIdx;
        
        if (this.skuField) this.skuField.value = sku.path;
        this._swatchSelected = true;
        this.onSelectCallback(subsetNode, { sku: sku, event: "subset-select", skuIdx: args.skuIdx });
    },
    
    toggleSelectedState: function(child, state) {
        if (this.multiShaded) return;
        if (state) {
            child.shadeNode.addClassName(this.selectedClass);
        } else {
            child.shadeNode.removeClassName(this.selectedClass);
        }
    },
    
    _addClassByColumn: function(swatch, positionIdx) {        
        // get column position
        var columns = this.columns;
        var col = 1;
        if (columns) {
            var num = (positionIdx + 1); // start from 1 instead of 0
            if (num > columns) {
                var multiplyby = Math.floor(positionIdx / columns);
                col = (num - (columns * multiplyby));
            } else {
                col = num;
            }
            // save class name to assign on mouseover
            // NOTE: saving for later since removing/adding classnames at this stage for all swatches would affect perfomance on prods w/ lots of skus
            swatch.columnClass = "thumb-col"+col;
            swatch.resetColumnClass = true;
        }
    }
});


/** Individual Swatch Classes **/

brand.product.swatch = Class.create(Widget,
{

    sku: null,
    name: null,
    type: "solo",
    selected: false,
    hex: null,
    rgb: "",
    idx: 0, // sku index, or for multi-shaded prods, position of shade in arrays for shade name, hex, etc...
    
    // smooshThumb: String
    // thumb jpg path: required for thumbSwatch, optional for hexSwatch
    smooshThumb: null, 

    initialize: function($super, arguments) { 
        this.setProperties(arguments);
        
        var sku = this.sku;
        this.smooshThumb = arguments.smooshThumb || sku.smoosh_thumb;
        this.hex = arguments.hex || (sku.color[0] ? sku.color[0].toString() : null);
        this.name = arguments.name || sku.shade_name;
        this.rgb = brand.hexToRGB(this.hex);
        
        $super();
    },
    
    postCreate: function() { 
        if ( this.rgb ) { 
            this._setTextColor(this.rgb);
        }
    },

    _onClick: function(e) {
        if (e) e.preventDefault();
        this.parent.setSwatch(this, { event: e });
    },
    
    _setTextColor: function(rgb) {
        if (!rgb || !this.tooltipNode) { return; }
        // if color is light, set text to black for appropriate contrast
        if (this._isBright(rgb)) {
            this.tooltipNode.style.color = "#000";
        }
    },
    
    _getBrightness: function(rgb) {
        if (!rgb) { return; }
        var total = rgb[0] + rgb[1] + rgb[2];
        return total;
    },

    _isBright: function(rgb) {
        if (!rgb) { return; }
        var isBright = this._getBrightness(rgb) > 450;
        return isBright;
    }
    
});


brand.product.hexSwatch = Class.create(brand.product.swatch,
{
    _swatchPath: "jsTemplates.product.hexSwatch",
    _swatchImagePath: "jsTemplates.product.hexSwatchImage",
     
    initialize: function($super, arguments) {
        this.setProperties(arguments);       
        this.templatePath = this._swatchPath;

        if ( this.sku.sku_multicolor_type ) { 
            this.type = this.sku.sku_multicolor_type;
        }
        if ( this.type === "duo" || ( this.type === "solo" && this._isDark(this.rgb) ) ) {
            this.templatePath = this._swatchImagePath;
        }
        
        $super(arguments);
    },
    
    _onMouseOver: function(e) {
        if (this.name && this.tooltipNode) {
            this.tooltipNode.style.visibility = "visible";
            this.domNode.style.zIndex = "10"; // fix for IE position+layering bug
        }
    },

    _onMouseOut: function(e) {
        if (this.name && this.tooltipNode) {
            this.tooltipNode.style.visibility = "hidden";
            this.domNode.style.zIndex = "1";
        }
    },

    _isDark: function(rgb) {
        if (!rgb) { return; }
        var isDark = this._getBrightness(rgb) < 100;
        return isDark;
    }
});


brand.product.thumbSwatch = Class.create(brand.product.swatch,
{
	_thumbPath: "jsTemplates.product.thumbSwatch",
	_thumbPathiPad: "jsTemplates.product.thumbSwatchiPad",
	
    resetColumnClass: true,
	
	

    initialize: function($super, arguments) {
        this.setProperties(arguments);
		
		  if ( global.isipad ) {
            this.templatePath = this._thumbPathiPad;
        }else{
			this.templatePath = this._thumbPath;
			}
        
        // rb keys for template
        if (!this.parentIsMultiShaded) {
            this.text_select = site.product.rb["select"];        
            this.text_toshop = site.product.rb.to_shop;
        }
        
        $super(arguments);
    },
    
    postCreate: function($super) {
        this.shadeNode = this.domNode.select("A")[0];
        this.containerClasses = this.domNode.className || "";
        var smooshThumbNode = this.shadeNode.select("IMG")[0];
        try { // IE: avoid choking on invalid hex values
            smooshThumbNode.style.backgroundColor = this.hex;
        } catch(err) {}
        
        // attach event handler here instead of via widget class (workaround
        // for widget class not passing event & event needed for preventDefault)
        this.domNode.observe("click", this._onClick.bind(this));
        
        if (this.name) {
            // tooltip workaround for IE (handled via CSS for other browsers)
            if (generic.env.isIE && this.tooltipNode) {
                this.domNode.observe("mouseover", this._showToolTip.bind(this));
                this.domNode.observe("mouseout", this._hideToolTip.bind(this));
            } else {
                this.domNode.observe("mouseover", this._addClass.bind(this));
            }
        }
        
        $super();
    },

    _addClass: function(e) {
        if (!this.resetColumnClass || !this.columnClass) return;
        this.domNode.className = (this.containerClasses + " " + this.columnClass);
        this.resetColumnClass = false;
    },
    
    _showToolTip: function(e) {
        this._addClass(e);
        this.tooltipNode.style.visibility = "visible";
        this.domNode.style.zIndex = 50; // fix for IE position+layering bug
        this.shadeNode.style.zIndex = 50;
    },

    _hideToolTip: function(e) {
        this.tooltipNode.style.visibility = "hidden";
        this.domNode.style.zIndex = 1;
        this.shadeNode.style.zIndex = 1;
    }
    
});
 
brand.product.videoPlayer = {
    config: {},
    lastCuePoint: 0,
    totalCuePoints: 0,
   
    init: function() {   
        //console.log("brand.product.videoPlayer.init"); 
        
        this.text_step = site.product.rb.step;
        this.text_of = site.product.rb.of;
        
        this.config.cue_points = false; // suppress cue points. Remove this to pick up cue points from /flash/makeup_artistry/js/video_VIDEO-NAME.js 
        // for videos with cue points (deprecated)
        if (this.config.cue_points) {
            if(this.config.cue_points.length) {
                this.totalCuePoints = this.config.cue_points.length - 1;
                this.lastCuePointTime = Number(this.config.cue_points[this.totalCuePoints - 2].time);
            }
        }
        
        this.videoPlaceholder =  $("flash_placeholder");
        if (!this.videoPlaceholder) return;
        this.productContainer = $("video_prod_container");
        
        this.drawVideoPlayer();
        this.processRelatedProducts();
        
        // for videos with cue points (deprecated)
        if (this.config.cue_points) {
            var self = this;
            generic.events.observe("videoPlayer:cuePoint", self.setCuePoint.bind(self));
        }
        
        // for videos with cue points (deprecated)
        // make sure flash has loaded before trying to set cue points
        if (this.config.cue_points) {
            var timerCount = 0;
            var playerIsReady = function() {
                timerCount++;
                //console.log("playerIsReady: checking");
                var vp = $("howToVideoPlayer");
                try {
                    var queryTime = vp.queryTime();
                }
                catch(err) { console.log("video player vp.queryTime error: "+err) }
                if (vp && typeof queryTime !== "undefined") {
                    self.playerFlashObject = vp;
                    clearInterval(timer);
                    self.processRelatedProducts();
                    self.setInitialView();
                } else if (timerCount > 10) {
                    clearInterval(timer);
                    console.log("getting method 'queryTime' of flash object failed.  giving up.");
                }  
            }  
            var timer = setInterval(playerIsReady, 500);
        }
    
    }, 
  
    drawVideoPlayer: function() {
        //console.log("brand.product.videoPlayer.drawVideoPlayer");
        var placeholderNode = this.videoPlaceholder;
        if (!placeholderNode) return;
        var params = { 
            bgcolor: "#000000",
            flashvars: { 
                conf_uri: placeholderNode.getAttribute("conf_uri"),
                showTitle: placeholderNode.getAttribute("show_title") || "true",
                pageName : placeholderNode.getAttribute("page_name")
                //videoHeight: placeholderNode.getAttribute("video_height") || 276
            }  
        };
        
        if (placeholderNode.getAttribute("color") === "white") {
            params.flashvars.color = "white"; 
        } 
        
        var attr = {
            id: "howToVideoPlayer",
            name: "howToVideoPlayer",
            data: "/flash/_video_player/howToVideoPlayer_query.swf" 
        };
        
        generic.flash.embed(attr, params, "flash_placeholder"); 
    },
  
    processRelatedProducts: function() { 
        //console.log("brand.product.videoPlayer.processRelatedProducts");
        if (!$("vid_prods")) return; 
           
        //if (this.productContainer) this.productContainer.addClassName("hidden");
        // for videos with cue points (deprecated)
        if (this.config.cue_points) {
            $$("#vid_prods .rel_prod").invoke('addClassName', "hidden");
            $$("#vid_prods .swatch_hex_container").invoke("show");
        }
        //console.log('$$("#vid_prods .swatch_hex_container") = ',$$("#vid_prods .swatch_hex_container"));
        
        brand.mpp.item.init({
            data: page_data.video_products,
            initButtons: true,
            video_prod: true,
            type: "video"
        }); 
    
    },
   
    setInitialView: function() {
        if (this.productContainer) {
            this.productContainer.addClassName("hidden");
        }
        $("tip_title").innerHTML = (this.config.title) ? this.config.title : "&nbsp;"; 
        $("tip_copy").innerHTML = (this.config.intro) ? this.config.intro : "&nbsp;";  
    },
  
    getTimeCode: function() {
        //console.log("brand.products.videoPlayer.getTimeCode");
        var vp = this.playerFlashObject;
        if (!vp) { return "0000"; }
        var time = Math.round(vp.queryTime()); 
        return time;
    },
  
    showCue: function(cidx) { 
        //console.log("brand.products.videoPlayer.showCue cidx: " + cidx); 
        if (cidx==-1) {
            this.setInitialView(); 
            return;
        } 
        
        var cue = this.config.cue_points[cidx];    
        var completed = (cidx == this.totalCuePoints);
        
        $("tip_title").innerHTML = (completed) ? "&nbsp;" : this.text_step + " " + (cidx+1) + " " + this.text_of + " " + this.totalCuePoints; 
        $("tip_copy").innerHTML = cue.copy;  
        
        var hasProducts = false;
        $$("#vid_prods .rel_prod").invoke('addClassName', "hidden");
        $$("#vid_prods .swatch_hex_container").invoke("hide");
        
        var str, productNode, productId, skuId, swatchNode;   
        cue.prods.each(function(catprodsku, idx) {
            //console.log("sku path: "+catprodsku); 
            catprodsku = catprodsku.strip();
            str = catprodsku.split("PROD")[1];
            productId = "PROD" + str.split("SKU")[0];
            skuId = str.split("SKU")[1];
            productNode = $("video_" + productId);
            swatchNode = $("video_swatch_SKU" + skuId);
        
            if (!!productNode) { 
                hasProducts = true;
                productNode.removeClassName("hidden");    
                if (!!swatchNode) swatchNode.style.display = "block"; 
            } 
        });
        
        if (hasProducts && this.productContainer) this.productContainer.removeClassName("hidden");  
    },
  
    setCuePoint: function(idx) { 
        //console.log("brand.products.videoPlayer.setCuePoint "); 
        var time = this.getTimeCode();
        if (time == "0000") { return; }
        
        var cuePoint = -1;  
        
        for (var i=0;i<=this.totalCuePoints;i++) { 
            if (time >= this.config.cue_points[i].time) {
                if (i==this.totalCuePoints) cuePoint = i; //outro 
            } else { 
                cuePoint = i-1;   
                break;
            }
        }  
        
        this.showCue(cuePoint); 
    }
    
};
// MPP functions
brand.mpp = brand.mpp || {};


// for mpp product modules
brand.mpp.item = {
   
    is_shaded: false,

    // input field (as add to bag btn) that holds sku path 
    // value of currently selected color sku
    skuField: "", 
    
    // page/content type
    type: "mpp",
    
    // this can be used when products which contain the same product
    // id are going to appear on the same page. Setting to true,
    // will append the path of the first sku instead of a product id to nodes
    altNodeId: false,
    
    // this is used in the case that products have the same id.
    // the type used if this is true is the time to ensure that
    // ids are unique for swatches
    altType: false, // SS note: needed?  Shouldn't be, but if it is, then it should use something other than a timestamp, like "type"
    
    init: function(args) {
        //console.log("brand.mpp.item.init: "+args.type);
    
        var products = args.data;
        var cartConfirmMsg;  
        var self = this;
        this.altNodeId = (args.altNodeId ? args.altNodeId : false );
        this.altType = (args.altType ? args.altType : false );
        if (args.type) { this.type = args.type; }
        var video_prod = args.video_prod || false;
        if (!products) return;
        this.skuButtons = {};

        products.each(function(prod, idx) {
            if (!prod.skus || !prod.skus[0]) { // ignore prod if no skus
                console.log("mpp: product "+prod.product_id+": "+prod.name+" has no skus in page_data");
                return;
            }
            var is_shaded = (prod.shaded == 1);
            var nodeId = prod.product_id;
            if (self.altNodeId) {
                nodeId = prod.skus[0].path;
            } else if (video_prod) {
                // use "video_PRODXXX" for nodeId
                nodeId = self.type +'_'+ nodeId;
            }
            prod.video_prod = video_prod;

            if (!$(nodeId)) return; // ignore if no html node associated with product
                
            // init cart confirm
            var created = false;
            if (!$("cart_confirm-" + nodeId)) {
                created = true;
                cartConfirmMsg = new brand.product.cartConfirm({
                    id: "cart_confirm-" + nodeId,
                    is_shaded: is_shaded,
                    prodName: prod.name,
                    nodeToReplace: $("cart_confirm_placeholder-" + nodeId)
                });
            } 
            
            var msgProps;
            if (is_shaded && args.shadedMessageProps) {
                msgProps = args.shadedMessageProps;                    
            } else if (args.messageProps) {
                msgProps = args.messageProps;                    
            }

            // set up product photo rollover
            if (prod.image_small_rollover) {
                self.initPhotoRollover(prod, nodeId);
            }
               
            if (is_shaded) {
                //console.log("nodeId = "+nodeId);
                // set up swatches
                if (created) {
                    var type = (self.altType ? (new Date()).getTime() : self.type);
                    self.initSwatch(prod, cartConfirmMsg, type, msgProps, nodeId);
                }
            } else {
                // non-shaded: 1st sku is default add-to-bag button value
               self.initButton(prod, cartConfirmMsg, msgProps, nodeId);
            }
            // init cart overlay functions for collection slideshow products
            if ($("cart_add_placeholder-collection-slideshow") || $("cart_add-collection-slideshow")) {
                type = "collection-slideshow";
                self.initSkuButton(prod, type, msgProps, nodeId);
            }
                
        }); // end forEach products
       
    },
    
    initSwatch: function(prod, cartConfirmMsg, type, msgProps, nodeId) {
        //console.log("initSwatch: "+nodeId + " " + $("swatch_colors_placeholder-" + nodeId));
        if (!$("swatch_colors_placeholder-" + nodeId)) return; // page_data has incorrect/extra content
        
        var smooshImgLg = prod.skus[0].smoosh;
        var skuFieldId = "prod_sku_cart_add-" + nodeId;        
         
        // init quick buy popover
        var cartAddMsg = this.initCartPopover(prod, nodeId, {
            is_shaded: true,
            skuFieldId: skuFieldId,
            smooshPath: (smooshImgLg.replace ? smooshImgLg.replace(/168x168/g, "56x56") : "/images/common/blank.gif"),
            smooshId: "smoosh_img_cart_add-" + nodeId,
            cartConfirm: cartConfirmMsg             
        });
        
        // mixin alternate properties for text values, favorites remove, etc...
        if (msgProps) cartAddMsg = Object.extend(cartAddMsg, msgProps);
        
        // sku field is in CartAdd template, so now it's available
        var skuField = $(skuFieldId);

        // init swatch color squares
        var swatch = new brand.product.hexSwatchSet({
            product: prod,
            skuField: skuField,
            productType: type,
            // on swatch select:
            onSelectCallback: function(selectedChild) {
                cartAddMsg.sku = selectedChild.sku; // quick buy
                cartAddMsg.show();
                cartConfirmMsg.sku = selectedChild.sku; // tell cartConfirm which sku has been selected
            }           
        }, $("swatch_colors_placeholder-" + nodeId));
    },

    // for add to cart triggers in addition to swatch trigger (e.g. collection non-flash animation)
    initSkuButton: function(prod, type, msgProps, nodeId) {
        //console.log("initSkuButton: "+nodeId + " " + $(type + "-sku-buttons-container-" + nodeId));
        //console.log("initSkuButton: "+type + "-sku-buttons-container-" + nodeId);
        if (!$(type + "-sku-buttons-container-" + nodeId)) return;
        
        var smooshImgLg = prod.skus[0].smoosh;
        var skuFieldId = "prod_sku_cart_add-" + type;        

        // init cart confirm
        if (!$("cart_confirm-" + type)) { // check if it's already been created
            this.skuButtons.cartConfirmMsg = new brand.product.cartConfirm({
                id: "cart_confirm-" + type,
                is_shaded: true, // TODO: set conditionally
                nodeToReplace: $("cart_confirm_placeholder-" + type)
            });
        }
            
        // init quick buy popover
        // shared popover for multiple prods/skus, so sku/prod-specific settings are set in click handler
        if (!$("cart_add-" + type)) { // check if it's already been created
            this.skuButtons.cartAddMsg = new brand.product.cartAdd({
                id: "cart_add-" + type, 
                skuFieldId: skuFieldId,
                smooshId: "smoosh_img_cart_add-" + type,
                cartConfirm: this.skuButtons.cartConfirmMsg,
                nodeToReplace: $("cart_add_placeholder-" + type)
            });
        }
        this.skuButtons.cartAddMsg.prodNameNode = this.skuButtons.cartAddMsg.domNode.select('.popover-title')[0];

        // mixin alternate properties for text values, favorites remove, etc...
        if (msgProps) cartAddMsg = Object.extend(cartAddMsg, msgProps);        
        // sku field is in CartAdd template, so now it's available
        var skuField = $(skuFieldId);
        var self = this;

        // init click for each sku image/button
        // sku/prod-specific settings
        prod.skus.each(function(sku, idx) {
            var buttonNode = $("sku-button-" + sku.sku_id);
            sku.prodName = prod.name;
            sku.imageSmall = prod.image_small; // prod-level thumb
            var isShaded = (prod.shaded ? true : false);
            //console.log("buttonNode = "+buttonNode+" sku.sku_id = "+sku.sku_id);
            if (!buttonNode) return;
            buttonNode.observe("click", function (clickEvt) {
                clickEvt.preventDefault();
                //console.log("sku = ",sku.sku_id);
                self.skuButtons.cartAddMsg.sku = sku; // quick buy
                self.skuButtons.cartAddMsg.is_shaded = isShaded;
                self.skuButtons.cartAddMsg.show();
                self.skuButtons.cartConfirmMsg.sku = sku; // tell cartConfirm which sku has been selected
                document.fire("swatch:click", { sku: sku, shadeIdx: idx, product: prod });
            });
        });
    },
    
    initButton: function(prod, cartConfirmMsg, msgProps, nodeId) {  
        //console.log("brand.mpp.item.initButton");        
        var skuFieldId = "prod_sku-" + nodeId;
        var skuField = $(skuFieldId);
        
        // if shoppable, init add button
        if ( skuField ) {        
            cartConfirmMsg.sku = prod.skus[0];
            skuField.value = prod.skus[0].path;
            
            var cartAddBtn = brand.product.addButton({
                addButtonNode: skuField,
                callback: function(response) {
                    //console.log("brand.product.initButton cartAddBtn: cartConfirmMsg");  
                    cartConfirmMsg.show({ response: response });
                }
            });
    
            // mixin alternate properties for text values, favorites remove, etc...
            if (msgProps) {
                cartConfirmMsg.setDisplayProperties(msgProps.confirm);
            }
        }
        
        // handle remove button for favorites
        if (this.type === "favorites") {
            var removeNode = $("btn_favorites_remove-" + nodeId);
            if (removeNode) {
                var removeBtn = brand.product.addButton({
                    addButtonNode: removeNode,
                    itemType: "favorites",
                    skuField: skuField,
                    action: "delete",
                    callback: function(response) {
                        if (msgProps.callbackRemoveButton) {
                            msgProps.callbackRemoveButton({ removeNodeId: removeNode.id, skuFieldValue: removeNode.id });   
                        }
                    }
                }); 
            }
        }
    },

    initCartPopover: function(product, nodeId, args) {
        //console.log("brand.mpp.item.initCartPopover: "+this.type+" product = ",product.name);       
        var cartAddClass = brand.product.cartAdd;
        var popArgs = {
            id: "cart_add-" + nodeId,
            is_shaded: false,
            prodName: product.name,
            price: product.price,
            product_price_with_tax: product.price_with_tax,
            nodeToReplace: $("cart_add_placeholder-" + nodeId)
        }
        popArgs = Object.extend(popArgs, args);

        if (this.type === "favorites") {
            cartAddClass = brand.product.cartAddFromFavorites;
            popArgs.isRemovable = true;
        }
        var cartAddMsg = new cartAddClass(popArgs); 
        
        return cartAddMsg;
    },
    
    initPhotoRollover: function(product, containerId) {
        var containerNode = $(containerId);
        if (!containerNode) return;
        var imgNode = containerNode.select("a img.thumb")[0];
        var img = product.image_small;
        var altImg = product.image_small_rollover;       
        if (!img || !altImg || !imgNode) return;
        var preloaded = new Image();
        preloaded.src = altImg; 
       
        var over = function(e) {
            e.target.src = preloaded.src;
        }
    
        var out = function(e) {
            e.target.src = img;
        }
        imgNode.observe("mouseover", over);
        imgNode.observe("mouseout", out);   
    }
}


/**
 * favorites cart & swatch functionality
 */
brand.mpp.initFavorites = function() {
    //console.log("site.product.favoritesMpp");
    var data = page_data.catalog.mpp.products;
    var itemContainerNode = $("favorites-product-container");

    var removeFavorite = function(args) {
       //console.log("site.product.favorites.removeButton "+args.removeNodeId+"/"+args.skuFieldValue);  
       if (!args||!args.removeNodeId) return; 
       var catprodsku = args.removeNodeId.split("-")[1];
       if (!catprodsku) return; 
       
       var productNode = $(catprodsku); 
       var swatches = productNode.select(".swatchset-hex-container")[0];
       var removeNode;
       
       // if product is unshaded or has only 1 swatch, remove the product
       if (!swatches || swatches.select(".swatch_hex_container").length==1) { 
           removeNode = productNode;
       }
       // else just remove the swatch
       else if (args.skuFieldValue) {
           var skuId = args.skuFieldValue.split("SKU")[1]; 
           removeNode = $("swatch_SKU"+skuId);
       }
       
       //console.log("removeNode: "+removeNode.id);
       if (removeNode) {
           removeNode.remove();
           
           // check for empty list
           if (itemContainerNode) {
               var items = itemContainerNode.select("div.rel_prod");
               if (items.length < 1) {
                   var noItemNode = $("no-favorites-message");
                   if (noItemNode) noItemNode.removeClassName("hidden");
                   itemContainerNode.hide();
               }
           }
       }
    };
    
    site.mpp.item.init({
        data: data,
        initButtons: true,
        type: "favorites",
        
        // configure confirm messages for cart or favorites remove
        messageProps: {
            callbackRemoveButton: removeFavorite 
        },
        shadedMessageProps: {
            callbackRemoveButton: removeFavorite  
        }

    });  
};
brand.product.productOverlay = Class.create(Widget, { 

    isOpen: false,
    position: {},
    lockToNode: null,
    templateString: "",
    baseClass: "",
    widgetsInTemplate: false,
    itemCount: 0,

    initialize: function($super, arguments) {
        //console.log("brand.product.productOverlay.init: "+arguments.id); 
        $super(arguments); 
    },
    
    open: function(args) {
        if (this.isOpen) { return; }
        var self = this;

        brand.overlay.launch({
            foregroundNode: this.domNode,
            displayInline: true,
            removeOnHide: false,
            onClose: function() {
                self.onClose();
            }
        });

        if (this.lockToNode) {
            // calculate height to get top offset 
            var t, h = 0;
            h = this.domNode.getHeight();
            t = (h * -1);
            this.position = {offsetTop: t};
            // position in relation to specified node
            var offsetTop = (this.position.offsetTop ? this.position.offsetTop : 0);
            var offsetLeft = (this.position.offsetLeft ? this.position.offsetLeft : 0);
            this.domNode.clonePosition(this.lockToNode, { setWidth: false, setHeight: false, offsetLeft: offsetLeft, offsetTop: offsetTop });
        } else {
            if (this.position.top) this.domNode.style.top = this.position.top+"px";
            if (this.position.left) this.domNode.style.left = this.position.left+"px";
        }       
        this.isOpen = true;
    },
    
    show: function(args) {
        //console.log("brand.product.productOverlay.show ",args);  
        //if (this.isOpen) { return; }
        if (this.isOpen) {
            this.close();
        }
        var response = (args && args.response ? args.response : null);
        this._updateDisplay(response);
        this.open(args);
    },
    
    getErrors: function(response) {
        var errors;
        var key;
        if (response && response.getMessages()) {
            errors = {};
            var messages = response.getMessages();
            for (var i = 0; i < messages.length; i++) {
                if (messages[i].text) {
                    key = messages[i].key;
                    errors[key] = messages[i];
                }
            }
        }
        return errors;
    },
    
    close: function() {
        //node.style.left = "-5000px";
        //this.domNode.style.display = "none";
        //this.isOpen = false;
        brand.overlay.hide();
        this.onClose();
    },
    
    // NEEDED?
    onClose: function() { 
        this.isOpen = false;
        //if (this.closeCallback) this.closeCallback();
    }
    
});


brand.product.cartAdd = Class.create( brand.product.productOverlay,
{
    //templatePath: "/js/brand/product/templates/cartAdd.html",
    templatePath: "jsTemplates.product.cartAdd",
    
    is_shaded: false,
    _enabled: true,
    
    // sku: Object
    // sku object from page_data
    sku: null,
    
    // template vars
    prodName: "",
    smooshPath: "/images/common/blank.gif",
    hex: "",
    price: "",
    smooshImgStore: {},
    
    initialize: function($super, arguments) {
        //console.log("brand.product.cartAdd init, args = ",arguments);
        this.templateString = false; //overrides productOverlay  
        $super(arguments);
    },
    
    postCreate: function() {
        //console.log("brand.product.cartAdd postCreate "+this.id);
        if (this.smooshId) this.smooshNode = $(this.smooshId);
        var button = (this.skuField ? this.skuField : $(this.skuFieldId));
        var self = this;
        if (button) {
            // add to cart button
            var prodBtn = brand.product.addButton({
                addButtonNode: button,
                callback: function(response) {
                    if (self.callback) {
                        self.callback(response);
                    }
                    self.close();
                    self.cartConfirm.show({ response: response });
                }
            });
        }
        this.priceNode = this.domNode.select('span.popover-price')[0];
    },
    
    setConfirmProperties: function(args) {
        // used to set display properties of confirm on CartAdd popover callback (ex: for favorites vs. checkout message)
        this.cartConfirm.setDisplayProperties(args);
    },
    
    show: function($super, arguments) {
        generic.events.fire({event:"productmessage:cartadd/show", msg:this.sku});
        //console.log("brand.product.cartAdd.show: ", this.prodName); 
        $super(arguments); 
    },
    
    _updateDisplay: function() {
        var sku = this.sku;
        //alert("CartAdd._updateDisplay: sku = "+Object.toJSON(this.sku));
        if ( this.smooshNode ) {
            this.smooshNode.style.backgroundColor = sku.color[0];
            this.smooshNode.src = this.smooshPath; // sets to blank to reset before loading next
            if (this.is_shaded) {
                var smooshImg = (sku.smoosh_thumb ? sku.smoosh_thumb : sku.smoosh);
                if (typeof smooshImg === "object") {
                    smooshImg = sku.smooshImg[0];
                }
                smooshImg = (!sku.smoosh_thumb ? smooshImg.replace(/168x168/g, "56x56") : smooshImg);
            } else if (sku.imageSmall) {
                var smooshImg = sku.imageSmall;
            }
            var imgStore = brand.loadImage({
                node: this.smooshNode,
                imagePath: smooshImg,
                imageStore: this.smooshImgStore,
                imgId: sku.sku_id
            });
            this.smooshImgStore = imgStore;
        }
        if (this.prodNameNode && sku.prodName) this.prodNameNode.update(sku.prodName); // default mpp's don't need to update product name & price. skuButtons do.
        if (this.priceNode) this.priceNode.update(sku.formatted_price);
        
        // shade name for products with a single shade
        if (sku.sku_multicolor_type && (typeof sku.shade_name === "object")) {
            this.swatchTitleNode.innerHTML = "";
        } else {
            this.swatchTitleNode.innerHTML = sku.shade_name;
        }
        
        // set sku value
        var button = (this.skuField ? this.skuField : $(this.skuFieldId));
        button.value = sku.path;

        // inventory status
        site.product.inventoryStatus({
            shoppable: sku.shoppable,
            message: sku.inventory_status_message,
            messageNode: this.inventoryStatusNode,
            buttonNode: this.addToBagNode,
            containerNode: this.domNode
        });
        
        // "finish" name
        this.finishNameNode.innerHTML = (sku.finish ? "("+sku.finish+")" : "");
    }

});


brand.product.cartAddFromFavorites = Class.create( brand.product.cartAdd, {

    // isRemovable: Boolean
    // Use/show remove button
    isRemovable: true,
    
    postCreate: function($super, arguments) {    
        $super(arguments); 
        //console.log("brand.product.cartAddFromFavorites.postCreate");
           
        // set up remove button
        if (this.isRemovable) {     
            var self = this;      
            var removeBtn = brand.product.addButton({
                addButtonNode: self.removeNode,
                skuField: $(self.skuFieldId),
                itemType: "favorites",
                action: "delete",
                callback: function(response) {
                    if (self.callbackRemoveButton) {
                        self.callbackRemoveButton({ removeNodeId:self.removeNode.id, skuFieldValue:$(self.skuFieldId).value });
                    }
                    self.close();
                    //self.cartConfirm.show({ response: response });
                }
            });        
            this.removeNode.removeClassName("hidden");
        }
    }

});


brand.product.cartConfirm = Class.create( brand.product.productOverlay,
{
    //templatePath: "/js/brand/product/templates/cartConfirm.html",
    templatePath: "jsTemplates.product.cartConfirm",
     
    is_shaded: false,
    isSized: false,
    prodName: "",
    useLeftAlign: false,
    
    // sku: Object
    // sku object from page_data
    sku: null,
    
    // type: String
    // item type for count in cookie (optional) ex: "favorites"
    type: "cart",
    showingErrors: false,
    
    initialize: function($super, arguments) {
        // default text
        this.text_addedMessageCheckout = site.product.rb.added_to_shopping_bag;
        this.text_addedMessageFavorites = site.product.rb.added_to_favourites;
        this.text_add_to_bag = site.product.rb.add_to_bag;
        this.text_thank_you = site.product.rb.thank_you;
        this.text_favorites = site.product.rb.favorites;
        this.text_checkout = site.product.rb.checkout;
        this.text_sorry = site.product.rb.sorry;
        this.text_continue_shopping = site.product.rb.continue_shopping;
    
        $super(arguments);
    },
  
    postCreate: function() {
        //this.cartHandler = generic.checkout.cart;
        this.shadeNameDash = this.shadeNameNode.innerHTML;
    },
    
    show: function($super, arguments) {
        var confirmNode = this.domNode;
        if (this.useLeftAlign) {
            confirmNode.addClassName("pop-confirm-align-left");
            confirmNode.removeClassName("pop-confirm-align-default");
        } else {
            confirmNode.addClassName("pop-confirm-align-default");
            confirmNode.removeClassName("pop-confirm-align-left");
        } 
        $super(arguments); 
    },
    
    setDisplayProperties: function(args) {
       // used to set display properties of confirm (ex: for favorites vs. checkout message) 
       brand.updateProperties.apply(this, [args]);
    },
    
    _updateDisplay: function(response) {
        //console.log("brand.product.cartConfirm._updateDisplay "+response.getError()); 
        
        // check for error
        var hasErrors = response ? response.getError() : false;
        var hasFssMessage = false;
        var hasPreviousErrors = this.showingErrors;
                 
        // show error
        if (hasErrors) {  
            this.errorMessageNode.innerHTML = response.getMessages() ? response.getMessages()[0].text : ""; 
            this.cartConfirmErrorNode.removeClassName("hidden");
            this.cartConfirmDisplayNode.addClassName("hidden");
            this.showingErrors = (hasErrors ? true : false); // save error state
        } 
        
        // show confirm message 
        else {
            // reset error display in case last state was showing errors
            if (hasPreviousErrors) {
                this.cartConfirmErrorNode.addClassName("hidden"); 
                this.cartConfirmDisplayNode.removeClassName("hidden");
            }
            //this.itemCount = this.cartHandler.getItemCount(this.type);
            //this.itemCount = this.cartHandler.getTotalItems();   
            //this.itemCountNode.innerHTML = this.itemCount.toString();
            this.prodNameNode.innerHTML = ((this.prodName && this.prodName.length > 1) ? this.prodName : this.sku.prodName);
            // populate shadeName node with shade name or size info
            if ((this.sku.shade_name && !Object.isArray(this.sku.shade_name)) || (this.isSized && this.sku.product_size)) {
                var skuInfo = this.sku.shade_name;
                if (this.isSized && this.sku.product_size) skuInfo = this.sku.product_size;
                this.shadeNameNode.innerHTML = this.shadeNameDash + skuInfo;
            } else {
                this.shadeNameNode.innerHTML = "";
            }
            
            if (this.type === "favorites") {
                this.domNode.addClassName("cart-confirm-overlay-container-favorites");
                this.addedMessageNode.innerHTML = this.text_addedMessageFavorites; 
            } else {
                this.domNode.removeClassName("cart-confirm-overlay-container-favorites");      
                this.addedMessageNode.innerHTML = this.text_addedMessageCheckout;
                var data = response.getData();
                var fssMessage = "";
                var fssMessageNode = this.cartConfirmDisplayNode.select(".cart-confirm-fss-message")[0];                
                if (fssMessageNode && data && data.trans_data) {
                    fssMessage = data.trans_data.free_shipping_message;
                    if (fssMessage && fssMessage.length > 1 && fssMessage != null) {
                        hasFssMessage = true;
                        fssMessageNode.innerHTML = fssMessage;
                        fssMessageNode.style.display = "block";
                    }
                }
                if (!hasFssMessage) fssMessageNode.hide();
            }
        }
    }

});


brand.product.swatchCard = Class.create( brand.product.cartAdd,
{

    templatePath: "jsTemplates.product.swatchCard",
    //templatePath: "/js/brand/product/templates/swatchCard.html",
    
    skuPath: "",
    closeOnClickOutside: null,
    swatchIdx: null, // idx to id single sku shades
    
    initialize: function($super, arguments) {
        this.setProperties(arguments);
        this.price = this.price;
        var sku = this.skus[0];
        this.text_limited = site.product.rb.limited;
        this.text_macpro = site.product.rb.macpro;
        $super(arguments, true);
        if (this.multiShaded) {
            this.domNode.addClassName("swatchcard-container-multishaded");
        }
    },

    postCreate: function($super) {
        // set nodes specfic to this class
        var id = this.id;
        this.skuField = $("prod-sku-"+id);
        this.shadeNameNode = $("shade-name-"+id);
        this.smooshNode = $("smoosh-img-"+id);
        this.descriptionNode = $("shade-description-"+id);
        this.finishNode = $("shade-finish-"+id);
        this.finishDescriptionNode = $("shade-finish-description-"+id);
        this.limitedNode = $("limited-flag");    
        this.proNode = $("pro-flag");
        this.inventoryStatusNode = $("inventory-status-"+id);
        $super();
        
        // if popover should close when user clicks outside of it
        if (this.closeOnClickOutside && this.closeOnClickOutside.enable) {
            var self = this; 
            $(document.body).observe("click", function(e) {        
                if (!self.isOpen) return;
                self.close();
            });
    
            var stopPropagation = function(e) {
                e.stopPropagation();
            }
    
            this.domNode.observe("click", stopPropagation);
    
            var nodesToExclude = this.closeOnClickOutside.nodesToExclude;
            if (nodesToExclude) {
                nodesToExclude.each(function(node) {
                    node.observe("click", stopPropagation);
                });
            }
        }
    },

    onSwatchSelect: function(args) {
        //console.log("brand.product.swatchCard.onSwatchSelect ",args);
        this.sku = args.sku;
        var swatchNode = args.swatchNode;
        this.skuField.value = this.sku.path;
        var useLeftAlign = false;
        this.swatchIdx = (args.swatchIdx >= 0 ? args.swatchIdx : null);
        // get child of swatchNode: swatchNode doesn't return proper values
        // from clonePosition (because of float?)
        this.lockToNode = swatchNode.select(".swatch-thumb")[0];
        
        // get column position of swatch thumb
        var getRightColumn = function() {
            var classes = $w(swatchNode.className)
            var classCount = classes.length;
            if (classCount.length < 1) return false;
            var hasClass = false;
            
            var searchIn = ["thumb-col5", "thumb-col6", "thumb-col7"];
            for (var i=0; i<classCount; i++) {
                if (searchIn.indexOf(classes[i]) != -1) {
                    hasClass = true;
                    break;
                }               
            }
            
            return hasClass;
        }
        useLeftAlign = getRightColumn();
        
        // add class by column position
        var node = this.domNode;
        if (useLeftAlign) {
            node.addClassName("pop-card-align-left");
            node.removeClassName("pop-card-align-default");
        } else {
            node.addClassName("pop-card-align-default");
            node.removeClassName("pop-card-align-left");
        }
        // pass these settings to the cart confirm obj
        var cartConfirm = this.cartConfirm;
        cartConfirm.setDisplayProperties({ type: "cart", lockToNode: this.lockToNode, useLeftAlign: useLeftAlign });
        
        // close other popovers if open
        if (cartConfirm.isOpen) {
            cartConfirm.close();
        }      
        this.show();
    },
    
    _updateDisplay: function() {
        //console.log("swatchCard._updateDisplay: sku = "+this.sku);
        var sku = this.sku;
        var swatchIdx = this.swatchIdx;
        var smoosh, imgId, shadeName, description;
        
        // data per single sku item array (multi-shaded)
        if (this.multiShaded && (swatchIdx >= 0)) {
            smoosh = sku.smoosh[swatchIdx];
            imgId = (sku.sku_id + swatchIdx);
            shadeName = (Object.isArray(sku.shade_name) ? sku.shade_name[swatchIdx] : sku.shade_name);
            description = sku.shade_description[swatchIdx];
            
        } else {
        // data per individual sku
            smoosh = sku.smoosh;
            imgId = sku.sku_id;
            shadeName = sku.shade_name;
            description = sku.shade_description;
            
            // content that displays for solo/duo skus
            // mac pro & limited flags
            if (this.limitedNode) {
                if (sku.limited_life == 1) {
                    this.limitedNode.show();
                } else {
                    this.limitedNode.hide();
                }
            }
            if (this.proNode) {
                if (sku.pro_product == 1) {
                    this.proNode.show();
                } else {
                    this.proNode.hide();
                }
            }
            
            // update inventory status
            site.product.inventoryStatus({
                shoppable: sku.shoppable,
                message: sku.inventory_status_message,
                messageNode: this.inventoryStatusNode,
                containerNode: this.domNode
            });         
        }
        
        this.smooshNode.src = this.smooshPath; // sets to blank to reset before loading next
        var imgStore = brand.loadImage({
            node: this.smooshNode,
            imagePath: smoosh,
            imageStore: this.smooshImgStore,
            imgId: imgId
        });
        this.smooshImgStore = imgStore;
        
        this.shadeNameNode.update(shadeName);
        // finish node content suppressed due to duplicate finish name in most shade name data
        //this.finishNode.innerHTML = (sku.finish && sku.finish_description ? "("+sku.finish+")" : "");
        //this.finishDescriptionNode.update(sku.finish_description);
        this.descriptionNode.update(description);
    }
    
});
brand.checkout = brand.checkout || {};

brand.checkout = {
    abort: false,
  
    makeExitBtn: function() {
        //console.log("btn_exit_checkout");
        // exit checkout button: sends user to last page before entering checkout
        var btn_exit_checkout = $("btn_exit_checkout"); 
        
        if (btn_exit_checkout) {
            btn_exit_checkout.observe("click", function() {   
                var loc = "/"; // home page default 
                var ref = document.referrer; 
                // redirect to home if coming from account section
                // have a feeling there will be more of these...
                if (ref && (ref.indexOf(".maccosmetics")>-1) && (ref.indexOf("checkout")==-1) && !(/account/g.test(ref)) && !(/macpro/g.test(ref)) ) {
                    loc = ref;
                }
                window.location = loc;
            });
        } 
    } 
};

        
brand.checkout.cartStatus = {  
    countNodeId: "global_cart_count",
    countContainerId: "shopping_bag_items", 
    
    init: function() {
        //console.log("brand.checkout.cartStatus.init");
        this.countNode = $(this.countNodeId); 
        var self = this;
          
        generic.events.observe("cart:countsUpdated", function(args) { 
            self.updateCount();
        });
          
        var countContainer = $(this.countContainerId);
        if (countContainer) {
            countContainer.removeClassName("hidden");
        } 
         
    }, 
    updateCount: function() {
        //console.log("brand.checkout.cartStatus.updateCount");
        if (this.countNode) { 
            this.countNode.innerHTML = generic.checkout.cart.getTotalItems();
        }
    }
};
brand.account = brand.account || {};

brand.account.panel = {
    // summary: 
    //      Sets default states for "my account" link headers
    hasSetPanelLinks: false,
    accountConfig: null,

    init: function(accountConfig) { 
        //console.log("brand.account.panel.init, accountConfig = ",accountConfig);  
        if (!$("accountnav") || !accountConfig) { return; }
        var self = this;
        this.accountConfig = accountConfig;
        this.setState();
         
        this.setPanelLinks(); // in case the links are in panel_open
     
        //subscribe to "my mac" left nav onclick for non-account/checkout pages
        generic.events.observe("globalnav:getcontent/my_mac", function(event) {
            self.handlePanelRefresh({event: "show_panel"});
        });   
    },
    
    handlePanelRefresh: function(args) { 
        brand.account.panel.setPanelLinks();    
    }, 
    
    setPanelLinks: function() {
        //console.log("brand.account.setPanelLinks "+this.hasSetPanelLinks);
        if (this.hasSetPanelLinks) return;
    
        $$(".signout_link").each(function(s) { 
            this.hasSetPanelLinks = true;
            s.observe("click", brand.account.panel.signoutSubmit ); 
        });  
    }, 
    
    signoutSubmit: function() {
         ///console.log("brand.account.signoutSubmit"); 
         var onSuccess = function() {
             generic.events.fire({event:"cartCount:reset",msg:0});  
             location.replace('/account/signin.tmpl'); 
         }
         var onFailure = function() {
             console.log("brand.account.signoutSubmit: SIGNOUT failure");
         } 
         generic.jsonrpc.fetch({method:'rpc.form', params:[{'_SUBMIT':'signout'}], onSuccess:onSuccess, onFailure:onFailure});
    },
    
    setState: function() { 
        //console.log("brand.account.setState ");
        var pn = page_data.panel_nav["default"]; 
        var section, subsection, activeSection;
        var sections = this.accountConfig.sections
        if (pn) {
            try {
                section = pn.id;
                if (pn.item) subsection = pn.item.id;           
            }
            catch (err) { 
                console.log("setState: ",err);  
            }
        }

        if (section !== "account") return;
        
        // account landing id
        if (subsection === "index" || !pn.item) {
        	subsection = "account_index";
        }
         
        // find "active" subsection 
        for (i=0; i<sections.length; i++) {
            //console.log("test: "+sections[i] + " / " + section + " / " + subsection); 
            if (sections[i] === section || sections[i] === subsection) {
                activeSection = sections[i];  
                break;
            }
        }                
         
        // set section/subsection image states 
        var activeImg = $("hd_"+activeSection);
       
        // set section/subsection image states
        this.setImages(activeSection);
        
        // sign in image state:
        // page_data for sign in may masquerade as other pages so check content
        // to identify whether current page is signin page
        var signinContainerNode = $("signin-container");
        var signinLinkNode = $$("#signin_link img")[0];
        if (signinContainerNode && signinLinkNode) {
            var signinImg = new brand.img(signinLinkNode, ["sel"]);
            signinImg.changeSrc("sel");
        } else if (signinLinkNode) {
            new brand.rollover(signinLinkNode, null);
        };
    },
    
    setImages: function(activeSection) {
        // apply rollovers to each non-active section image
        // show sel state for active section image        
        var activeImg = "hd_"+activeSection;
        $$("#accountnav img.accountnav_hd").each( function(node) {
            if (activeImg !== node.id) {
                var r = new brand.rollover(node, null);
            } else {
                var aImg = new brand.img(node, ["sel"]);
                aImg.changeSrc("sel");
            }
        });
    }
};

/**
 * Email sign up for utility nav
 */
brand.account.emailSignup = {
    emailSignupJsonRpcPath: "email.signup",
     
    init: function(args) { 
        var self = this;
        var submitNode = args.submitNode;
        this.fieldNode = args.fieldNode;
        if (!submitNode || !this.fieldNode) return;
        submitNode.observe('click', self.validateEmail.bind(self));
        this.fieldNode.observe('keypress', self.validateEmail.bind(self));
    },
    
    validateEmail: function(event) { 
        if (!this.fieldNode) { return; } 
        
        if ( event.type === "keypress" && (event.keyCode != Event.KEY_RETURN) ) { 
            return false;           
        }
        var email = this.fieldNode;
        var popup; 
        var onSuccess = function() {
            brand.overlay.launch({
                foregroundNode: $("pop_email_valid"),
                displayInline: true,
                removeOnHide: false,
                displayDuration: 5000
            });
        }
        var onFailure = function() {
            brand.overlay.launch({
                foregroundNode: $("pop_email_invalid"),
                displayInline: true,
                removeOnHide: false,
                displayDuration: 5000
            });
        }
        var requestArgs = [{
            EMAIL_ADDRESS: email.value 
        }]; 
            
        if (brand.forms.isEmailAddress(email.value)) { 
            var d = generic.jsonrpc.fetch({method:this.emailSignupJsonRpcPath, params:requestArgs, onSuccess:onSuccess, onFailure:onFailure});
        } else { 
            onFailure();
        }
       
        Event.stop(event);

        return false;
    }
};
brand.view = {};

/**
 * Add css classnames to form elements for IE6
 * @memberOf brand.view
 */
brand.view.setFormSelectors = function() {
    //console.log("brand.setFormSelectors");
    $$("INPUT[type=text]").invoke("addClassName","input-text");
    $$("INPUT[type=password]").invoke("addClassName","input-password"); 
    $$("INPUT[type=image]").invoke("addClassName","input-image"); 
}

/**
 * Apply rollover behavior to images w/ class "rollover"
 * @memberOf brand.view
 */
brand.view.initRollovers = function() {
    $$("img.rollover").each(function(el) { 
        var rollover = new brand.rollover(el, null); 
    }); 
}


/**
 * brand.view.colorNav
 * swf bordering globalnav that displays a clickable gradient of colors
 * Navigates to color browser
 * @memberOf brand.view
 */
brand.view.colorNav = {
   abort: false,
   placeholder: "color_nav_placeholder",
   flashid: "color_nav",
   offW: 8,
   onW: 90,
   timer: null,
 
   embed: function() { 
        if (brand.view.colorNav.abort) return;
        
        if (generic.env.isSafari) {
            var cn = $("color_nav_container");
            if (cn) { cn.style.display = "none"; }
            this.placeholder = "color_nav_placeholder_standalone";
            this.flashid = "color_nav_standalone";
        } 
        
        var params = {
            menu: "false",
            movie: "/flash/color_nav/color_nav.swf",
            flashvars: {
                gradient_uri: "/flash/color_nav/assets/color_gradient.png",
                application_uri: "/flash/color_play/index.tmpl",
                application_query_string: "?colorplaysample="
            }
        };
    
        var attr = {
            id: this.flashid,
            name: this.flashid,
            data: "/flash/color_nav/color_nav.swf",
            width: this.offW
        };
        
        if (generic.flash.playerversion) {
            attr.playerversion = generic.flash.playerversion;
        }
 
        if ($(this.placeholder)) generic.flash.embed(attr, params, this.placeholder);  
    },
    setWidth: function(e) {    
        if (e=="mouseover") { 
             clearTimeout(brand.view.colorNav.timer);  
             $(this.flashid).style.width = this.onW + "px";  
        } else {     
             brand.view.colorNav.timer = setTimeout(function(){    
                 $(brand.view.colorNav.flashid).style.width = brand.view.colorNav.offW + "px";     
             }, 600);
        }
    
    }    
}

/*
 * brand.view.utilityNav
 * sets up visual toggling of utility nav button fields
 * hooks up email & locator fields to emailSignup & locator classes if applicable
 * NOTE: search initialized from brand.search Class
 * @memberOf brand.view
 */
brand.view.utilityNav = {
    
    formelements: {},    
    buttonsets: {},
    minTop: 460,//60px to account for two new nav items.400, // minimum num of pixels from top of body to beginning of utility nav
    
    init: function(args) {

        // button/form field toggling
        var container = $("utilitynav");
        if (!container) return false;
        var nodes = container.select('.utilitynav_button');
        var self = this;
        if (args.minTop) this.minTop = args.minTop;

        nodes.each(function(node) {
            // button/form pair toggling relies on naming convention: utilitynav_[button or form]_[pair name]
            if (!node.id) return;
            var name = node.id.replace(/utilitynav_button_/g, "");
            var formnode = $("utilitynav_form_" + name);

            // get form element id's being used (not all locale's may have same elements)
            //var ftext = dojo.query("input[type=text]", formnode);
            var fieldnode = formnode.select("input[type=text]")[0];            
            var fieldsubmit = formnode.select("input[type=image]")[0];
            
            if (name && formnode && fieldnode) {
                self.buttonsets[node.id] = name;
                self.buttonsets[fieldnode.id] = name;
                self.buttonsets[fieldsubmit.id] = name;
                
                self.formelements[name] = { field: fieldnode, submit: fieldsubmit };
                node.observe("click", function() {
                    self.showForm(node);
                });
                fieldnode.observe("blur", function() {
                    self.showButton(fieldnode);
                });
                fieldsubmit.isfocused = false;
                fieldsubmit.onfocus = function() { 
                    this.isfocused = true;
                }
                fieldsubmit.observe("blur", function() {
                    self.showButton(fieldsubmit);
                });
            }       
        });

        // form submits: email signup & locator     
        if (this.formelements.email) {
            this.initEmailSignup(this.formelements.email);
        }
        if (this.formelements.locator) {
            this.initLocator(this.formelements.locator);
        }

    },

    showForm: function(node) {
        var shownode, hidenode;
        var hidenode = node;
        var name = this.buttonsets[node.id];
        var shownode = $("utilitynav_form_" + name);
        this.toggle(hidenode, shownode);
        
        // focus on field
        var focusnode = (this.formelements[name] ? $(this.formelements[name].field) : null);
        if (!focusnode) { return; }
        focusnode.focus();
        focusnode.value = "";
        focusnode.isfocused = true;
    },

    showButton: function(node) {
        var shownode, hidenode;
        node.isfocused = false;
        var name = this.buttonsets[node.id];
        var self = this;
        var submitbtn = $(this.formelements[name].submit);
        var field = $(this.formelements[name].field);
        var hidenode = $("utilitynav_form_" + name);
        var shownode = $("utilitynav_button_" + name);
        
        // wait for animation to finish
        var pause = function() {
            if (!submitbtn.isfocused && !field.isfocused) {
                self.toggle(hidenode, shownode);           
            }
        }
        setTimeout(pause, 200);
    },
    
    toggle: function(hidenode, shownode) {
        if (hidenode && shownode) {
            hidenode.style.display = "none";
            shownode.style.display = "block";
        }
    },
    
    initEmailSignup: function(formelements) {
        if (site.account && site.account.emailSignup) {
            site.account.emailSignup.init({
                submitNode: formelements.submit,
                fieldNode: formelements.field
            });
        } else {
            console.log("(site || brand).account.emailSignup not found");
        }

    },
    
    initLocator: function(formelements) {
        if (site.locator) {
            site.locator.init({
                submitNode: formelements.submit,
                fieldNode: formelements.field
            });
        } else {
            console.log("(site || brand).locator not found");
        }

    }
}


brand.view.flashPopover = { 
    params: {  
        wmode: "transparent"
    },
    attr: {
        id : "page_overlay",
        name: "page_overlay",
        height: "84",
        width:"245",  
        bgcolor: "#000" 
    },
    embed: function() {
        if (!$("page_overlay_div")) return; 
        var params = this.params;
        var attr = this.attr;
        params.movie = $("page_overlay_div").getAttribute("swf");
        attr.data = $("page_overlay_div").getAttribute("swf");
        generic.flash.embed(attr, params, "page_overlay_div");  
    },
    close: function() {  //called by shipping swf
        $("page_overlay_container_div").addClassName("page_overlay_container_div_closed"); 
        //console.log("brand.view.flashPopover.close()");
    }   
}
      

/*
 * brand.view.footer
 * Applies behavior to footer container and country chooser menu
 * @memberOf brand.view
 */
brand.view.footer = {  
    adjust: function() {

        if (generic.env.isIE6) {
            var footerNode = $("footernav");
            this.initIELayerFix(footerNode);
            var footer = new brand.bottomFixed({
                node: footerNode
            });
        }

        if (!global.isipad) {
            var utilityNav = new brand.bottomFixed({
                node: $("utilitynav"),
                minTop: brand.view.utilityNav.minTop
            });
        }
        
        /* Not for NA
        //country chooser drop-up
        var footerMenu = new brand.menu({
            menu: "countries_container",
            target: "countries_hd"
        }); 
        */
    
    },
    
    // IE workaround:
    // Puts an iframe behind the footer in IE so windowed elements (<select>'s) don't show through
    initIELayerFix: function(footer) {
        if (!footer) return;
        ///includes/blank.html
        var iframe = new Element('iframe', { "id": "footer_iframe", "src": "/includes/blank.html", "frameborder": "0",  "marginwidth": "0", "marginheight": "0", "scrolling": "no" });
        footer.insert({ "top": iframe });
        var footerContent = $("footernav_content");
        var offset = iframe.offsetHeight; 
        // move the footer content up so it's on top of the iframe
        if (footerContent) {
            footerContent.style.marginTop = "-" + offset + "px";
        }
    }
};
    

/*
 * brand.view.heightHandler
 * manage height changes for/caused by absolutely positioned elements
 * @memberOf brand.view
 */
brand.view.heightHandler = { 
    pagetype: null,
    min: 620,
    winh: 0, // re-saved everytime window height changes
    bodyh: 0,
    bodyhOriginal: 0, // onload is saved as original height of body
    bodyhWithoutPanel: 0, // current body height w/out additional height of sliding panel
    offset: 0, // height as offset by footer (calculated on init)
    isCMS: false,
    spacer: null, // node to resize to affect body height
    isLoading: false,
    isResizing: false,
    timer: null,
    isIE6: false,
    hasCMSLayers: false,
    
    pagetypeAttributes: {
        "checkout" : { exclude: true },
        "flash_landing" : { fixedScrolling: true, fillsWindow: true }, // ex: home
        "image_landing" : { fixedScrolling: true, fillsWindow: true }, // ex: home non-flash slideshow
        "full_window_flash" : { fillsWindow: true }, // ex: custom palette
        "full_window" : { fillsWindow: true }, // ex: full window image & ipad home page (which has no fixed scrolling)
        "flash_browser" : { excludeOnload: true, excludeOnResize: true }, // contain their own resize functions // NOTE: excludeOnResize USED TO JUST be color_play, now we're trying OTHER PRODUCT BROWSERS HERE
        "locator" : { excludeOnload: true }
    },
    
    init: function() {
        // get page type
        if (this.pagetype) { 
            type = this.pagetype;
        } else {
            try {
                this.pagetype = page_data.panel_nav["default"].id;
            }
            catch (e) { };
        }
        var pageAttributes = this.pagetypeAttributes[this.pagetype];
        //console.log("heighthandler: this.pagetype = "+this.pagetype);
        
        if (pageAttributes && pageAttributes.exclude) { return; }
        var gnavNode = $("globalnav_container");
        if (!gnavNode) { return; } // ignore pages that don't have the global nav
        
        var spacer = $("column_spacer");        
        if (generic.env.isIE6) {
            this.isIE6 = true;
            var colorNav = $("color_nav_td");
            spacer = (colorNav ? colorNav : spacer); // if no color nav, don't set its height 
        }
        this.spacer = spacer;

        // get offset for footer
        var fnnode = $("footernav");
        this.offset = ((fnnode && !global.isipad) ? fnnode.offsetHeight : this.offset); 

        // get minimum height of nav (and therefore page)
        var navMinHeight = gnavNode.getStyle("min-height");
        if (navMinHeight) navMinHeight = parseInt(navMinHeight, 10);
        this.min = (navMinHeight && (!isNaN(navMinHeight)) ? navMinHeight : this.min);
        
        // handle CMS content cases
        this.isCMS = (page_data ? page_data.cms_generated : null);
        var cmslayers = $$("#main_content_td .cms_layer");
        this.hasCMSLayers = !!cmslayers.length;
        var useSetCMSHeightContainer = $$("#main_content_td .set-cms-container-height")[0];
        if (this.hasCMSLayers && useSetCMSHeightContainer) {
            this.setCMSHeight(useSetCMSHeightContainer, cmslayers); // ex: newsworthy
        } else if (this.isCMS || this.hasCMSLayers) {
            this.cmsCleanup(cmslayers);
        }
        
        // ensure that winh doesn't result in negative value later
        this.winh = this.getWindowHeight();
        if (this.winh <= this.offset) {
            this.winh = (this.offset * 2);
        }
       
        // SS NOTE: home was being excluded here, so put in equivalents, but not sure why it was being excluded
        if (!pageAttributes || (pageAttributes && !pageAttributes.fillsWindow)) {
            this.bodyhOriginal = document.body.scrollHeight; 
            this.bodyh = this.bodyhOriginal; 
        }
        
        this.onLoad(); 

        // handlers for events that can change window/body size
        var self = this;
        generic.events.observe("accordion:open", function(event) {
            self.onNavChange("show", event);
        });
        generic.events.observe("panelnav:show", function(event) {
            self.onNavChange("show", event);
        });
        // handle psubnavs that load items via ajax & could be tall enough to push beyond fold on initial display of their content: whats new only case for now
        generic.events.observe("panel:prodcat", function(event) {
            if (event.request && (event.request.id === "psubnav_whatsnew")) {
                self.onNavChange("load", {id: event.request.id});
            }
        });        
        generic.events.observe("panelnav:hide", function(event) {
            self.onNavChange("hide", event);
        });
        if (!pageAttributes || (pageAttributes && !pageAttributes.excludeOnResize)) {
            Event.observe(window, "resize", function() {
                self.onResize();
            });
        }
        
        this.isLoading = false;        
    },

    onLoad: function() {
        //console.log("heighthandler.onLoad");
        var pageAttributes = this.pagetypeAttributes[this.pagetype];
        if (pageAttributes && pageAttributes.excludeOnload) {
            if (!this.isIE6) {
                //console.log("heighthandler.onLoad: excluding");
                return;
            } else if (!$("color_nav_container")) {
                return;
            }
        }
        this.isLoading = true;        
        var h = (this.winh > this.min) ? this.winh : this.min; 
        var spacer = this.spacer;
  
        // home page: set height of spacer column to match window
        if (pageAttributes && pageAttributes.fillsWindow) {  
            h = (this.winh - this.offset);
            this.spacer = spacer = $("main_content");
            spacer.style.height = h + "px";
            
        // default: for sub pages
        } else {  
       
            // if body is shorter than window, set height to match window
            if (h >= this.bodyh) {    
                //console.log("body shorter than window");
                spacer.style.height = h + "px";  
                
            // IE6: cases where taller body requires explicitly setting spacer height 
            } else if (this.isIE6) {
                var colorNav = $("color_nav");  
                if (colorNav) { 
                    var maxh = (2800 > this.bodyh) ? 2800 : this.bodyh;
                    colorNav.style.height = this.bodyh + "px";
                    setTimeout(function() {
                        colorNav.style.height=maxh+"px";
                    }, 2000);  
                          
                }
                
                spacer.style.height = this.bodyh + "px"; 
            
            // for flash browsers, set spacer height to at least window initially, since flash could resize itself to shorter than window w/out triggering the resize event
            //} else if (this.pagetype === "flash_product_browser") {
            //    spacer.style.height = h + "px"; 
            }
        }

        this.bodyhOriginal = this.bodyh = this.bodyhWithoutPanel = h;
    },

    onNavChange: function(action, args) {
        var pageAttributes = this.pagetypeAttributes[this.pagetype];
        //if (pageAttributes && pageAttributes.fixedScrolling) return;
        //console.log("brand.view.heightHandler: onNavChange");
        
        if (this.isResizing) { return; }
        var type = args.type;
        var parentId = args.parentId;
        // ignore gnav accordion
        if (parentId === "globalnav_container" && type === "accordion") {
            return;
        }              
        var spacer = this.spacer;
        var id = args.id;
        if (type === "panel") {
            this.activePanelId = id;
        }
        
        var panel = $(this.activePanelId);
        if (!panel) { return; } // ex: open panels
        this.isResizing = true; // set isResizing after all non-returnable conditions met
        var self = this;
                
        //alert("/panelnav/event/"+action+", id = "+id+" panel to check = "+this.activePanelId+" type = "+type+" panel node = "+panel+" parent id = "+parentId);
    
        // wait for animation to finish
        var pause = function() {
            if (action === "hide" && type === "panel") {
                var h = self.bodyhWithoutPanel;
                // compare pre-panel open body height against window height
                if (h < self.winh) {
                    h = self.winh;
                }
                //console.log("CLOSE: h = "+h+" self.bodyhWithoutPanel = "+self.bodyhWithoutPanel);
                spacer.style.height = h + "px";
                self.bodyh = h;
            } else if (action === "show" || action === "load") {            
                var panelh = panel.offsetHeight;
                // compare against window & last known body height
                // (getting current body height here not consistent across browsers)
                if (panelh > self.winh && panelh > self.bodyh) {
                    spacer.style.height = panelh + "px";
                    self.bodyh = panelh;
                }           
            }
            self.isResizing = false;
        }
        setTimeout(pause, 600); // note: should be equal to or greater than duration of open/close Panel sliding       
    },
    
    onResize: function() {
        if ((this.isResizing || this.isLoading) && !this.isIE6) { return; }
        var winh = this.getWindowHeight();
        var bodyh = this.bodyh;        
        var page = this.pagetype;
        var pageAttributes = this.pagetypeAttributes[page];

        if (winh > (bodyh + this.offset)) {
            this.doResize(page, winh);
        } else if (!global.isipad && (winh < (bodyh - this.offset)) && pageAttributes && pageAttributes.fillsWindow) { // page is supposed to fill window & winh is less than bodyh
            if ((winh > this.min)) {
                this.doResize(pageAttributes, winh);           
            }
            // else: window and/or content height are less than minimum, so don't resize

        // workaround for IE6: when contents in main body change dynamically (ex: elements load/display after page load), then we have to get/set height again
        } else if (this.isIE6) {            
            bodyh = document.body.scrollHeight;
            if (bodyh != this.bodyh) {
                this.doResize(page, bodyh);
                this.bodyh = bodyh;
            }
        }
       
        this.winh = winh;
    },
    
    doResize: function(pageAttributes, elementh) {
        var timer = this.timer;
        var spacer = this.spacer;
        var offset = this.offset;
        var h = elementh;
        var self = this;
        var resize = function() {
            if (pageAttributes && pageAttributes.fillsWindow) { h = (elementh - offset); }
            spacer.style.height = h + "px";
            self.bodyh = self.bodyhWithoutPanel = h;
        };      
    
        // avoid IE resize recursion
        if (this.isIE6) {
            if (timer) clearTimeout(timer);
            timer = setTimeout(resize, 300);
        } else {
            resize();
        }     
    },
    
    getWindowHeight: function() {
        var h;
        if (typeof window.innerHeight !== 'undefined') {
            h = window.innerHeight;
        } else {
            h = document.documentElement.clientHeight;
        }
        return h;
    },
    
    setCMSHeight: function(container, cmslayers) {
        //console.log("setCMSHeight");
        var h = 0;
        var nodes = cmslayers;
        if (cmslayers.length==0) { return 800 }; /** cms_generated flag inaccurately set **/
        var thisNodeBottom;
        var lastNode = cmslayers[0];
        var lastNodeBottom = 100; // arbitrary non-zero min
        // figure out which node is last on page
        cmslayers.each(function(node, ix) { 
            thisNodeBottom = parseInt(node.style.top) + parseInt(node.style.height);
            if (lastNode && lastNodeBottom) {
                if (lastNodeBottom < thisNodeBottom) {
                    lastNodeBottom = thisNodeBottom;
                    lastNode = node; 
                }
            }
        });
        // use top+height value of last node to set height for whole container
        container.style.height = (lastNodeBottom + 40 + "px"); // add 40 slop
    },
    
    cmsCleanup: function(cmslayers) { 
        var cmsBlocks = []; 
        cmslayers.each(function($_) {  
            pushNew(cmsBlocks,$_.ancestors()[0]);
        });
        cmsBlocks.each(function($_) { 
            if (!$_.hasClassName("noCMSCleanup")) {
                parseCMSLayers($_);
            }
        });
        //console.log("heightHandler.cmsCleanup");
      
        function parseCMSLayers(parentDiv) {
            //console.log("heightHandler.cmsCleanup: parentDiv = ",parentDiv);
            var moved = false; 
            if (parentDiv.hasClassName("hidden")) {  
                moved = true;
                parentDiv.origLeft = parentDiv.style.left;  
                parentDiv.style.left = "-5000px";
                parentDiv.removeClassName("hidden");
            }
            var input = parentDiv.select(".cms_layer");  
            var output = input.sort(function(a,b){return parseInt(a.style.top) - parseInt(b.style.top)}); 
            
            var outputRows = [];
            var outputFinal = [];
            outputRows[0] = [];
            var oIndex = 0;
            var previousTop = 0;
        
            for (var i=0;i<output.length;i++) { 
                output[i].style.height = "auto";  
                output[i].cmsTop = parseInt(output[i].style.top); 
                output[i].actualHeight = parseInt(output[i].clientHeight);   
                output[i].impliedTopMargin = (i==0) ? output[i].cmsTop : output[i].cmsTop - output[i-1].cmsTop - output[i-1].actualHeight; 
                //console.log(output[i].id + " " + output[i].actualHeight);
                previousTop = (i==0) ? output[i].style.top : output[i-1].style.top;
                
                if (output[i].style.top==previousTop) {//same row
                     
                } else {//next row
                    outputRows[oIndex] = outputRows[oIndex].sort(function(a,b){return  parseInt(a.style.left) -  parseInt(b.style.left)});    
                    oIndex++; 
                    outputRows[oIndex] = [];
                }
                outputRows[oIndex].push(output[i]);
            }
            outputRows[oIndex] = outputRows[oIndex].sort(function(a,b){return  parseInt(a.style.left) -  parseInt(b.style.left)}); 
      
            var o = {}; var css = ""; 
            var adjust = (parentDiv.id == "main_content_td" ) ? 476 : 0;
            for (var i=0;i<outputRows.length;i++) { 
                for (var j=0;j<outputRows[i].length;j++) { 
                    o = outputRows[i][j]; 
                    css = "position:relative;"
                    css += "width:" + o.style.width + ";";  
                    css += "height:" + o.actualHeight + "px;";
                    css += "margin-left:" + (parseInt(o.style.left)-adjust) + "px;";
                    css += "margin-top:" + o.impliedTopMargin + "px;"; 
                    o.style.cssText = css;  
                    parentDiv.appendChild(o);
                } 
            }   
     
            if (moved) { 
                 parentDiv.addClassName("hidden")
                 parentDiv.style.left = parentDiv.origLeft; 
            }  
        }
    
        function pushNew(arr,o) {  
            var n = true;
            for (var i=0;i<arr.length;i++) {
                if (arr[i]==o) {n = false;break;}
            }
            if (n) arr.push(o);   
        }

/*       
        try {
            //var colorNav = (generic.env.isSafari) ? $("color_nav_standalone") : $("color_nav");  
            //colorNav.style.height = ($("main_table").scrollHeight + $("footernav").clientHeight + 50) +  "px";
            this.spacer.style.height = ($("main_table").scrollHeight + this.offset + 50) +  "px";
console.log("this.spacer.style.height = "+this.spacer.style.height);
        } catch(e) { console.log("cmsCleanup error = "+e) }
*/
    }   
};

brand.view.home = {
    leftoffset: 194,
    init: function() {
        var flashPlaceholder = $("main_bkg_div");
        this.containerNode = $("homepage_flash_container");
        // ipad version of home
        if (global.isipad) {
            this.initJSSlideshow();
            return;
        } // end: ipad version of home
        
        if (!flashPlaceholder) return;
        brand.view.heightHandler.pagetype = "flash_landing"; // assumes 'position: fixed' scrolling for regular home page
        
        // alternate type for content that needs to scroll normally
        //brand.view.heightHandler.pagetype = "flash_browser";

        if (generic.env.isFF) { 
            // hack: force main flash to wait for color strip to load
            brand.view.home.embedSplash.delay(0.5);
        } else {
            brand.view.home.embedSplash();

            // mimic position:fixed for ie6
            var setBottom = function() {
                var fixedFlashTest = new site.bottomFixed({
                    node: brand.view.home.containerNode,
                    bottom: 18, // height of footer
                    startingTopPosition: 0,
                    observeResize: false
                }); 
            };
            if (generic.env.isIE6 && brand.view.heightHandler.pagetype === "flash_landing") {
                setBottom.delay(1);
            }
        }
    },
    embedSplash: function() {
        var containerNode = brand.view.home.containerNode;
        if (!containerNode) return; // homepage_flash_container
        if (brand.view.getLocaleSuffix) var fileSuffix = brand.view.getLocaleSuffix("flash_home");
       
        var params = { 
            // wmode: "transparent",
            wmode: "opaque", 
            allowScriptAccess: "always",
            flashvars: {
              "assetDomain" : "/flash/home_page/assets/",
              "assetsDomain" : "/flash/home_page/assets/",
              "conf_uri" : "/flash/home_page/xml/config" + fileSuffix + ".xml" 
              // "conf_uri" : "/flash/home_page/xml/wonderWoman" + fileSuffix + ".xml"
            }
        };
       
        var attr = {
            id : "main_bkg",
            name: "main_bkg",
            data: "/flash/HomepageSlideshow.swf", 
            bgcolor: "#000000" 
            // data: "/flash/home_page/assets/swf/index_ww.swf",
            // bgcolor: "#FFFFFF" 
        };  
      
        //generic.flash.embed(attr, params, "main_bkg_div");
        
        // check for flash embed status
        var timerCount = 0; 
        var checkFlashLoading = function() {
            timerCount++;
            // if flash embed worked, set resizing
            if ($("main_bkg")) {
                clearInterval(timer);
                // scrollable home page content        
                //site.view.sizeContainer({ container: containerNode, setHeight: true, //minimumHeight: minh, setWidth: false }); // Venomous Villains
                //Event.observe(window, "resize", function() {
                //    site.view.sizeContainer({ container: containerNode, setHeight: true, minimumHeight: minh, setWidth: false });
                //});
                
                // regular full-window slideshow
                site.view.sizeContainer({ container: containerNode, leftoffset: site.view.home.leftoffset, setWidth: true, setHeight: false });
                Event.observe(window, "resize", function() {
                    site.view.sizeContainer({ container: containerNode, leftoffset: site.view.home.leftoffset, setWidth: true, setHeight: false });
                });
            } else if (timerCount > 2) {
                // if flash embed failed, use js slideshow
                clearInterval(timer);
                brand.view.home.initJSSlideshow();
                console.log("loading hp slideshow instead of flash, timerCount");
            }  
        }
        // give flash embed JS time to modify the DOM before checking for the re-rendered html
        var timer = setInterval(checkFlashLoading, 100);
        checkFlashLoading();
        
    },
    // non-flash version of slideshow
    initJSSlideshow: function() {
        brand.view.heightHandler.pagetype = (global.isipad ? "full_window" : "image_landing");
        var overlayMessage = $("fss-overlay-container");
        var noFlashPlaceholder = $("homepage-noflash-content");
        var previousScaleState;

        var scaleSlideshow = function(slidesNode, noFlashContainer, event) {
            if (generic.env.isIE6) { // workaround for IE6 loading contents at 0
                if (generic.env.isIE6 && event === "load") {
                    var mainContentHeight = $("main_content").style.height;
                    if (mainContentHeight) mainContentHeight = parseInt(mainContentHeight, 10);
                    noFlashContainer.style.height = (mainContentHeight && (!isNaN(mainContentHeight)) ? mainContentHeight+"px" : "100%");
                } else {
                    noFlashContainer.style.height = "100%";
                }
            }
            var viewportSize = document.viewport.getDimensions();
            var viewportWidth = (viewportSize.width - site.view.home.leftoffset);
            var viewportHeight = viewportSize.height;
            var scaleState = "full-width";
            var classToRemove = "slide-images-full-height";
            if (viewportWidth < (viewportHeight * 1.5)) { // 1.5 = image w:h ratio
                classToRemove = "slide-images-full-width";
                scaleState = "full-height";
            }
            //alert("viewportWidth = "+viewportWidth+" viewportHeight = "+viewportHeight+" scaleState = "+scaleState + " previousScaleState = "+previousScaleState+" event = "+event);
            if (scaleState !== previousScaleState) {
                slidesNode.addClassName("slide-images-" + scaleState);
                slidesNode.removeClassName(classToRemove);
                //console.log("adding class "+scaleState);
            }
            previousScaleState = scaleState;
        }

        var displayContent = function() {
            var noFlashContainer = $("front-slideshow-noflash"); // html container for pc (non-ipad) version
            var slidesNode = $("front-slides");
            if (!slidesNode) return;

            if (noFlashContainer) {
                noFlashContainer.style.display = "block"; 
                var containerNode = brand.view.home.containerNode;
                if (containerNode) containerNode.style.width = "100%"; // for non-flash slideshow, set width 100% on parent container
            }        
            // init slideshow
            if (typeof Slideshow !== "undefined" && slidesNode) {

              if (slidesNode) {
                  var progress = new brand.progress({
                      containerNode: slidesNode,
                      progressNode: $('slideshow-loading')
                  });
                  progress.start();
              }

                new Slideshow('front-slides', {
                    fadeDuration: 1,
                    delay: 4,
                    pauseBeforeNext: 1, // pause before next frame loads. gives time for last frame to fade completely or partially out
                    childrenToAnimate: {
                        className: "slide-image-logo",
                        delayShow: 1, // amount of time (in seconds) to delay fade in after main slide fades in
                        delayHide: 1, // amount of time (in seconds) to delay fade out before main slide fades out
                        fadeDuration: 1
                    },
                    onEnd: function() {
                      if(progress) {
                        progress.clear();
                      }

                      // display fss overlay
                      if(overlayMessage) {
                        brand.overlay.launch({ foregroundNode: overlayMessage, displayInline: true, removeOnHide: false });
                      }
                    }
                    
                });
            } else {
                // display fss overlay
                if(overlayMessage) {
                  brand.overlay.launch({ foregroundNode: overlayMessage, displayInline: true, removeOnHide: false });
                }
            }
            
            // scale images for variable pc window size
            if (!global.isipad) {
                Event.observe(window, "resize", function() {
                    scaleSlideshow(slidesNode, noFlashContainer, "resize");
                });
                scaleSlideshow(slidesNode, noFlashContainer, "load");
            }
        }
        
        // check if slideshow html needs to be loaded
        if (global.isipad || $("front-slideshow-noflash")) {
            // pc/non-flash: tmpl wouldn't already be loaded if the page's default/1st choice is to use the flash
            // ipad: tmpl include already loaded via perl
            displayContent(); // go ahead and display slideshow if html is ready    
        } else if (noFlashPlaceholder) {
            // grab slideshow html (Note: deferring here avoids loading slideshow content in background if flash is in effect)
            var linkNode = $("homepage-noflash-content-path");
            var tmplPath = (linkNode ? linkNode.href : null);
            if (!tmplPath) return;
            var req = new Ajax.Request(tmplPath, {
                method:'get',
                onSuccess: function(transport) {
                    var response = transport.responseText || "no response text";
                    noFlashPlaceholder.style.display = "block";
                    noFlashPlaceholder.update(response);
                    displayContent();
                },
                onFailure: function() {
                    console.log("initJSSlideshow: Error loading " + tmplPath);
                }
            });
        }
    }
};  

brand.view.collectionBrowser = {
    init: function() {
        if (brand.view.getLocaleSuffix) this.fileSuffix = brand.view.getLocaleSuffix("flash_collection_browser");
        
        //Looks Collection
        //brand.view.collectionBrowser.embedLooksSwf(); 

        //Picks Collection
        brand.view.collectionBrowser.embedPicksSwf();
        
        //What's New Collections
        brand.view.collectionBrowser.embedWhatsNewMedia();
        
        brand.view.collectionBrowser.embedLooksSlideshow();
    }, 
    embedLooksSwf: function() { 
        if (!$("looks_flash")) return;
        
        var placeholderNode = $("flash_placeholder");
        if (!placeholderNode) return;

        var movieId = "looksBrowser";
        var params = { 
            bgcolor: "#000000",
            flashvars: {
                conf_uri: "/flash/looks/xml/" + placeholderNode.getAttribute("conf_uri") + this.fileSuffix + ".xml",
                json_cat_id: placeholderNode.getAttribute("json_cat_id"),
                movieName : movieId
            } 
        };
    
        var attr = {
            id: movieId,
            name: movieId,
            data: "/flash/_looks_browser/looksBrowser.swf",
            width: 460,
            height: 370 
        };
    
        generic.flash.embed(attr, params, "flash_placeholder");
    }, 
    embedPicksSwf: function() { 
        if (!$("picks_flash")) return;

        var placeholderNode = $("flash_placeholder");
        if (!placeholderNode) return;

        var movieId = "picksBrowser";
        var params = {  
            bgcolor: "#000000",
            flashvars: {
                conf_uri: "/flash/picks/xml/" + placeholderNode.getAttribute("conf_uri") + this.fileSuffix + ".xml",
                json_cat_id: placeholderNode.getAttribute("json_cat_id"),
                movieName: movieId
            }
        };

        var attr = {
            id: movieId,
            name: movieId,
            data: "/flash/_picks_browser/picksBrowser.swf",
            width: 460,
            height: 370
        };

        generic.flash.embed(attr, params, "flash_placeholder");
    },
    embedLooksSlideshow: function() {  
        //console.log("brand.view.collectionBrowser.embedLooksSlideShow: "+page_data.catalog.mpp.media.looks);  
        var looksContainer = $("collection_looks");
        if (!looksContainer || !page_data.catalog.mpp.media.looks) return;   
         
        var slideshow = new brand.slideshow({ 
            loop: true,
            looks: page_data.catalog.mpp.media.looks,
            slide: $("slideshow_slide"), 
            header: $("slideshow_header"),
            link: $("slideshow_link"),
            nav: { left: $("slideshow_prev"), right: $("slideshow_next")} 
        });
            
        looksContainer.removeClassName("hidden"); 
    },
    
    embedWhatsNewMedia: function() {
        //check for flash_p for cases when img instead of swf
        var placeholderNode = $("flash_placeholder");
        if (!placeholderNode) {
            // init slideshow if relevant html found
            if (typeof Slideshow !== "undefined" && $("collection-slides")) {
                new Slideshow('collection-slides', { delay: 2.5, fadeDuration: 1.5 });
            }        
            return;
        }
        if (!page_data.catalog.flash_display_order) return;

        var movieId = "collectionBrowser";
        var params = {  
            bgcolor: "#000000",
            flashvars: {
                conf_uri: "/flash/collection_browser_example/xml/config" + this.fileSuffix + ".xml",
                json_cat_id: placeholderNode.getAttribute("json_cat_id"),
                display_order: page_data.catalog.flash_display_order.join(","),
                movieName: movieId
            }
        };

        var videoHeightAttribute = placeholderNode.getAttribute("videoHeight");
        var videoAttribute = placeholderNode.getAttribute("video");
        if (videoHeightAttribute && videoAttribute) {
            params.flashvars.videoHeight = videoHeightAttribute;
            params.flashvars.video = videoAttribute;
        }
    
        var attr = {
            id: movieId,
            name: movieId,
            data: "/flash/_collection_browser/collectionBrowser.swf",
            width: 475,
            height: 375
        };  
        
        generic.flash.embed(attr, params, "flash_placeholder"); 
    }
};
    
brand.view.productBrowser = {
    attr: {
        id : "productBrowser",
        name: "productBrowser",
        bgcolor: "#000000" 
    },
    init : function() {   
        // specify this to keep global height handler from changing height 
        brand.view.heightHandler.pagetype = "flash_browser";
        if (brand.view.getLocaleSuffix) this.fileSuffix = brand.view.getLocaleSuffix("flash_product_browser");
        var leftoffset = 210;
        var useSizeContainer = true;
    
        var container = $("productBrowser_resize");
        if (container.hasClassName("colorPlay")) this.embedColorPlay();
        if (container.hasClassName("brushPlay")) this.embedBrushFinder();
        if (container.hasClassName("mascaraFinder")) this.embedMascaraFinder();
        
        if (container.hasClassName("foundationFinder")) {
            this.embedFoundationFinder({ container: container });
            useSizeContainer = false;
        }

        if (container.hasClassName("backstage")) {
            this.embedBackstage({ container: container });
            useSizeContainer = false;
        }
        
        if (useSizeContainer) {
            brand.view.sizeContainer({ container: container, leftoffset: leftoffset });
            
            Event.observe(window, "resize", function() {
                site.view.sizeContainer({ container: container, leftoffset: leftoffset });
            }); 
        }
    }, 
    embedColorPlay: function() {    
        var params = { 
            flashvars: {
                conf_uri : "/flash/color_play/xml/config" + this.fileSuffix + ".xml",
                colorplaysample: generic.env.query("colorplaysample") || "",
                filters: generic.env.query("filters")  || "",
                movieName: this.attr.id,
                v: "20090402"
            } 
        };
        var attr = this.attr;
        attr.data = "/flash/_product_browser/productBrowser.swf"; 
        generic.flash.embed(attr, params, "flash_placeholder");  
    }, 
    embedBrushFinder: function() {   
        var params = { 
            flashvars: {
                "conf_uri" : "xml/config" + this.fileSuffix + ".xml",
                "movieName" : this.attr.id
            }
        };
        var attr = this.attr;
        attr.data = "/flash/_guide_browser/guideBrowser.swf";
        generic.flash.embed(attr, params, "flash_placeholder"); 
    }, 
    embedMascaraFinder: function() { 
        var params = {  
            flashvars: {
                "conf_uri" : "/flash/mascara_finder/xml/config_mascara" + this.fileSuffix + ".xml",
                "movieName" : this.attr.id
            }
        };
        var attr = this.attr;
        attr.data = "/flash/_guide_browser/guideBrowser.swf";
        generic.flash.embed(attr, params, "flash_placeholder"); 
    },
    
    embedBackstage: function(args) {
        var params = {
            flashvars: {
                "conf_uri" : "/flash/backstage/xml/config" + this.fileSuffix + ".xml",
                "movieName" : this.attr.id,
                "startCity": generic.env.query("city")
            }
        };
        var attr = this.attr;
        attr.data = "/flash/backstage/index.swf";
        generic.flash.embed(attr, params, "flash_placeholder");
        
        this.setHeight({ h: 670, container: args.container });
        Event.observe(window, "resize", function() {
            site.view.productBrowser.setHeight({ h: 670, container: args.container });
        });
    },

    embedFoundationFinder: function(args) {
        var params = {
            allowScriptAccess: "always",
            flashvars: {
                "conf_uri" : "/flash/foundation_finder/xml/config" + this.fileSuffix + ".xml",
                "movieName" : this.attr.id,
                "style" : 1
            }
        };
        var attr = this.attr;
        attr.data = "/flash/foundation_finder/foundation_finder.swf";
        generic.flash.embed(attr, params, "flash_placeholder");
        
        this.setHeight({ h: 670, container: args.container });
        Event.observe(window, "resize", function() {
            site.view.productBrowser.setHeight({ h: 670, container: args.container });
        });
    },

    setHeight: function(args) {   
        //console.log("productBrowser.setHeight args = ",args);
        //console.log("brand.view.heightHandler offset = "+brand.view.heightHandler.offset);
        try { 
            var contentHeight = args.h;
            var container = args.container;
            var containerMinHeight = args.containerMinHeight;
            var viewportHeight = document.viewport.getHeight();  
            var newHeight = (contentHeight > viewportHeight) ? contentHeight : viewportHeight;
            if (containerMinHeight) {
                newHeight = (containerMinHeight > newHeight) ? containerMinHeight : newHeight;
            }
            newHeight = (newHeight - site.view.heightHandler.offset) + "px"; 
            //console.log("productBrowser.setHeight: "+args.h + "/" + containerMinHeight +"/"+ viewportHeight + "/ " + newHeight);
            
            container.style.height = newHeight;    
        } catch(e) {
            console.log("productBrowser.setHeight e: "+e.description);
        }
    }
};

brand.view.artists = { 
    createRollOvers: function() {
        //console.log("brand.view.artists.createRollOvers");
        var last_popup;
        $$("img.artists-rollover").each( function(elem) {
            elem.observe("mouseover", function(e) {
                var popup_image = new Element("img");
                popup_image.className = "artist_pic";
                var popup_image_name = e.target.src;
                popup_image_name = popup_image_name.replace('114x114', '140x130_on');
                popup_image_name = popup_image_name.replace('jpg', 'png');
                popup_image.src = popup_image_name;
                popup_image.style.position = "absolute";
                if ( last_popup ) {
                    last_popup.style.display = "none";
                }
                popup_image.style.display = "block";
                
                // var img_coords = dojo.coords(e.target);
                var img = e.target; 
                /* 130 = height of replacement, 140 = width of replacement */
                popup_image.style.top = (img.positionedOffset().top - ((130 - img.getHeight())/2) + 2) + "px";
                popup_image.style.left = (img.positionedOffset().left - ((140 - img.getWidth())/2)) + "px";
                
                popup_image.observe("mouseout", function(e) {
                    e.target.style.display = "none";
                    e.target.parentNode.removeChild(popup_image);
                });
                
                e.target.parentNode.appendChild(popup_image);
                last_popup = popup_image;
            });
        });
    } 
};

brand.view.fromourlips = {
    init: function() {
        brand.view.heightHandler.pagetype = "flash_landing";

        // hack: force main flash to wait for color strip to load 
        if (generic.env.isFF) {
            brand.view.fromourlips.embedSplash.delay(3);
        } else {
            brand.view.fromourlips.embedSplash();
        }

    },
    embedSplash: function() {
        var movieId = "main_bkg";
        var params = {
            wmode: "opaque",
            flashvars: {
                "assetsDomain" : "/flash/vivaglam_201002/assets/",
                "conf_uri" : "/flash/vivaglam_201002/xml/config" + global.localeFileSuffix + ".xml",
                "movieName" : movieId
            }
        };

        var attr = {
            id : movieId,
            name: movieId,
            data: "/flash/vivaglam_201002/index.swf",
            bgcolor: "#FFFFFF"
        };

        generic.flash.embed(attr, params, "main_bkg_div");
    }
};

brand.view.customPalette = {
    init: function() {
        var flashPlaceholder = $("flash_placeholder");
        if (!flashPlaceholder) return;
        brand.view.heightHandler.pagetype = "full_window_flash";
        if (brand.view.getLocaleSuffix) this.fileSuffix = brand.view.getLocaleSuffix("custom_palette");            
        this.embed();
    },
    embed: function() {
        var cartId = generic.env.query("CART_ID");
        var collectionId = generic.env.query("COLLECTION_ID");
        
        var params = { 
            wmode: "transparent", 
            flashvars: {
                conf_uri : "/flash/custom_palette/xml/config" + this.fileSuffix + ".xml",
                "movieName" : "customPalette",
                v: "20100701"
            }
        };
        if (cartId) params.flashvars.cart_id = cartId;
        if (collectionId) params.flashvars.collection_id = collectionId;

        var attr = {
            id : "customPalette",
            name: "customPalette",
            data: "/flash/custom_palette/index.swf",
            bgcolor: "#000000", 
            width: "100%",
            height: "100%", 
            align: "top",
            vspace: 0,
            hspace: 0
        };
      
        generic.flash.embed(attr, params, "flash_placeholder");
    }
};

brand.view.getLocaleSuffix = function(id) {
    if (global && global.localeFileSuffixes && global.localeFileSuffixes[id]) {
        var fileSuffix = global.localeFileSuffixes[id];
    } else {
        var fileSuffix = global.localeFileSuffix;
    }
    return fileSuffix;
};


// Handle width setting for flash containers where needed
// (Height handled from site-wide brand.view.heightHandler unless specified to set height from here)
brand.view.sizeContainer = function(args) {
    var leftoffset = args.leftoffset || 190;
    var container = args.container;
    var setWidth = args.setWidth || false;
    var setHeight = args.setHeight || false;
    try {
        var viewportSize = document.viewport.getDimensions();
        if (setWidth) {
            var viewportWidth = viewportSize.width - leftoffset;
            container.style.width = viewportWidth + "px";
        }
        if (setHeight) {
            var viewportHeight = viewportSize.height;
            var minimumHeight = args.minimumHeight || 400; // arbitrary
            if (viewportHeight < minimumHeight) {
                container.style.height = "" + minimumHeight + "px";
            } else {
                container.style.height = "100%";
            }
            //console.log("container.style.height: "+container.style.height+" $(main_content).style.height = "+$("main_content").style.height+" site.view.heightHandler.offset = "+site.view.heightHandler.offset);
        }        
    } catch(e) {
        console.log("sizeContainer e: "+e);
    }
};
/** 
MK: some of this should move to /js/site/coremetrics/, especially the parts with hardcoded catprodskus  
**/

brand.coremetrics = {
    abort: false,
    init: function() {
        if (this.abort) return; 
        //this.panelNav.track();
        //this.livepopupTrack(); 
    },
    
    livepopupTrack: function() {
        generic.events.observe("livepopup:click", function(event) {
            cmCreateConversionEventTag("Live Chat", "1", "ASK AN ARTIST", "10");
        }); 
    }
};
 

var lpMTagConfig = {}; // externally-used global var for Live Person

/*
brand.coremetrics.panelNav = {
    cm_map : {}, // combined map of all cmcats on all nav categories
    cm_corrected_cat : {}, // map of ids to cmcats
    
    track: function() {  
        //this.createMap();
 
        document.observe("panelnav:show", function(event) {
        console.log("brand.coremetrics.panelNav.track: show / " + Object.toJSON(event.memo));
        if (event.memo.type === "panel"){
                       //brand.coremetrics.panelNav.panelClickOpen(event.memo); 
                    } else if (event.memo.type === "accordion") {
                       //brand.coremetrics.panelNav.open(event.memo); 
                    } 
    });
 
        document.observe("panelnav:hide", function(event) {
        console.log("brand.coremetrics.panelNav.track: hide / " + Object.toJSON(event.memo));
    //  brand.coremetrics.panelNav.panelClickClose(event.memo); 
    });       
};
*/
var Analytics = Class.create({
    subscribers : {},
    listeners : {},
    fromConfigListeners : [],
    seenMemoWithTag : {},
    isEnabled: false,
    enabledModules: new Array(),
    cm_map : {},
    cm_corrected_cat : {},
    localPath : '',
    macSkipCount : 0,
    prodCatData : {},
    uniqueEvents: [],
    swatchSkip : false,


    RPC_METHODS_ALLOWED: new Hash({
        "prodcat" : 1,
        "generic" : 1,
        "cart" : 1,
        "rpc.form" : 1,
        "search" : 1,
        "email.signup" : 1
    }),

    initialize: function(modules,enabled){
        this._addStaticListeners();
    },
    addPendingTags: function(newTags ){
        this.pendingTags = newTags;
        this.execTags();
        return this;
    },
    addElementEvents: function(newEvents){
        this.elementEvents = newEvents;
        
    },
    _espanolPrefix: function(){
        var espanol_var = window.location.host;
        var espanol_prefix = '';
        if (espanol_var.match("espanol") ) {
             var espanol_prefix = 'ESP : ';
        };
        return espanol_prefix;
    },
    _addStaticListeners: function(){
        var self = this;
        var espanolPrefix = self._espanolPrefix();
        document.observe("dom:loaded", function(){
            self.isEnabled = (typeof ANALYTICS_ENABLED != "undefined") ? ANALYTICS_ENABLED : false;
            self.enabledModules = (typeof ANALYTICS_MODULES != "undefined" ) ? ANALYTICS_MODULES : [];

            // CONVERSION EVENTS - If conversion events exist add handler as directed and form input for location
            self.jsEvents = (typeof CONVERSION_EVENTS != "undefined" ) ? CONVERSION_EVENTS : [];
            self.jsEvents = self.jsEvents.concat( (typeof JS_EVENTS != "undefined" ) ? JS_EVENTS : [] );
            self.jsPixelEvents = (typeof JS_PIXEL_EVENTS != "undefined" ) ? JS_PIXEL_EVENTS : new Object();

            if (Object.keys(self.jsPixelEvents).length > 0){
                Object.keys(self.jsPixelEvents).each(function(module){                                            
                    self.jsPixelEvents[module].each(function(CEVENT){                                            
                        self.jsEvents = self.jsEvents.concat( (typeof CEVENT != "undefined" ) ? CEVENT : [] );  
                    });                                                                                          
                });                                                                                               
            }

            //console.log("addStaticListeners");
            //console.log(self.jsEvents);

            if (typeof self.jsEvents != "undefined"){
                //console.log(self.jsEvents);

                self.jsEventsWaited = [];

                self.jsEvents.each(function(CEVENT){
                    CEVENT.hookIds = [];  // allow for mapping one event to many elements
                    //console.log("event on ", CEVENT);
                    //Check for events to wait on, loading iframes, bv etc to be custom handled in static listeners
                    //if the ele is expected to exist on load, then the event handlers can be set up right away in addFrontendEvent

                    if (CEVENT.after){
                        self.jsEventsWaited.push(CEVENT);
                    }else{
                        //_addFrontendEvent: function(myElement,myEvent,tagBlocks)
                        self._addFrontendEvent(CEVENT);
                    }

                });

            }
            // console.log( "Analytics enabled: ", self.isEnabled );
            // console.log( "Analytics registered modules: ", self.enabledModules );
            self.localPath = document.location.pathname;

            // I loath this, but events fire user generated or not and something has to supress them on direct urls to SPP, for example

            if ( self.localPath.match("product") ){
                self.macSkipCount = 3; 
            }
            if (self.localPath.match("account") ){
                self.macSkipCount = 1;
            }
            if (self.localPath.match("looks") ){
                self.macSkipCount = 3;
            }
            if (self.localPath.match("artists")){
                self.macSkipCount = 1;
            }
            if (self.localPath.match("category")){
                self.macSkipCount = 3;
            }
            if (self.localPath.match("whats_new")){
                self.macSkipCount = 2;
            }

            //if (typeof site != "undefined"){
            //    if (typeof site.globalnav != "undefined"){
                    // now called based on sitenav:loaded
                    //self.createMap();
            //    }
            //}
        });


        // Mac special start
        document.observe("panelnav:show", function(event) {

            var open_data = event.memo.msg;
            //console.log("brand.coremetrics.panelNav.track: show / ",open_data);

            if (open_data.type === "panel"){
                //console.log("panel show");            
                // if (typeof open_data.sectionId != "undefined"){
            
                self.addPendingTags({"CoreMetrics":{"dom:loaded":[{"params":[espanolPrefix + open_data.sectionId +" : "+ open_data.displayName  ,null,open_data.itemId,null,null],"tag":"cmCreatePageviewTag"}]}});

                var elementCat = "NAV";
                if (open_data.parentId !== 'globalnav_container'){
                    elementCat = elementCat + " : " + open_data.sectionId;
                }
                self.addPendingTags({"CoreMetrics":{"dom:loaded":[{"params":[open_data.displayName,elementCat,null,null,null,null],"tag":"cmCreatePageElementTag"}]}});

            } else if (open_data.type === "accordion") {
                //console.log("accordion show");
            }
        });

    document.observe("accordion:open", function(event) {
        //console.log(event);

        if ( self.macSkipCount ){
          self.macSkipCount--;
          return;
        } 
        var open_data = event.memo.msg;
        //console.log("brand.coremetrics.Accordion.track: open / ", open_data);
        var parentCat ='' 
        var parentCatName = '';
        var curCat = '';
        var prefix = 'MPP : ';
        if (open_data.parentId === 'globalnav_container'){
            prefix = '';
        }else{
            if ( open_data.parentId.match("CAT[0-9]+") ) {
                parentCat = open_data.parentId.match("CAT[0-9]+")[0];
                parentCatName = self.cm_map[parentCat];
            }
            else if (open_data.parentId.match("psubnav_") ) {
                // psubnav's don't get parentId's, instead, they get psubnav_{parent.displayName}
                parentCatName = open_data.parentId;
                parentCatName = parentCatName.replace("psubnav_","");
            }
            curCat = open_data.id.match("CAT[0-9]+")
            
        }
        // remove Shop Products and Makeup Artistry because they are special cases 
        if (open_data.displayName != "Nos Produits" && open_data.displayName != "L'Art Du Maquillage"){
            if (open_data.displayName != "Shop Products" && open_data.displayName != "Makeup Artistry"){
               if (open_data.displayName != "Productos" && open_data.displayName != "Arte en Maquillaje") { 
                self.addPendingTags({"CoreMetrics":{"dom:loaded":[{"params":[espanolPrefix + prefix + parentCatName + " : " + open_data.displayName,null,curCat,null,null],"tag":"cmCreatePageviewTag"}]}});
               }
            }
        }

        var elementCat = "NAV";
        if (open_data.parentId !== 'globalnav_container'){
            elementCat = elementCat + " : " + parentCatName;
        }

        self.addPendingTags({"CoreMetrics":{"dom:loaded":[{"params":[open_data.displayName,elementCat,null,null,null],"tag":"cmCreatePageElementTag"}]}});
    });



    // The below might be removable, but just commented out the tag line for now. 
    // SPECIAL HOOK for Mac catalog.tmpl work around, also note, page_data doesn't have any English names so we have to use results from the RPC not in page_data, saving here.
    document.observe("panel:prodcat", function(event){


       // Save for possible later use 
           self.prodCatData = event.memo.msg.data;
       if ( self.macSkipCount ){
          self.macSkipCount--;
          return;
        }
       var prodcatRequest = event.memo.msg.request;
       var thisCat = '';
       if (self.cm_corrected_cat[prodcatRequest.itemId] != "undefined"){
           thisCat = self.cm_corrected_cat[prodcatRequest.itemId];
           //console.log("corrected cat ",thisCat);
       }else{
           thisCat = prodcatRequest.itemId;
       } 
       //catID = page_data.catalog.collection.match("CAT[0-9]*"); 
       //self.addPendingTags({"CoreMetrics":{"dom:loaded":[{"params":["products : "+ prodcatRequest.parent.displayName  ,null,prodcatRequest.itemId,null,null],"tag":"cmCreatePageviewTag"}]}});
    
    });



    // this is a lot more specific to when we need a product view tag on a swatch click vs the generic swatch:click event. 
    document.observe("productmessage:cartadd/show", function(event){
       var cur_cat = event.memo.msg.path.match("CAT[0-9]*");
       var cur_prod = event.memo.msg.path.match("PROD([0-9]*)")[0];
       var pdcatalog = page_data.catalog;
       var productName = '';
       if (self.swatchSkip){
           return;
       }
            if (typeof(pdcatalog) != "undefined") {
                if (typeof(pdcatalog.mpp) != "undefined") {
                    if (typeof pdcatalog.mpp.products != "undefined"){
                        for (var i=0; i < pdcatalog.mpp.products.length; i++){
                            if (pdcatalog.mpp.products[i].category_id.match(cur_cat) && pdcatalog.mpp.products[i].product_id.match(cur_prod) ){
                                //console.log("GOT NAME ",pdcatalog.mpp.products[i].cm_name, cur_cat, pdcatalog.mpp.products[i].category_id); 
                                productName = pdcatalog.mpp.products[i].cm_name;
                            } 
                        }    
                    }else{
                        Object.keys(pdcatalog.mpp).each(function(CATEGORY){    
                           var CAT = pdcatalog.mpp[CATEGORY];
                           if (typeof CAT.products != "undefined"){
                               for (var i=0; i < CAT.products.length; i++){
                                   if (CAT.products[i].category_id.match(cur_cat) && CAT.products[i].product_id.match(cur_prod) ){
                                       //console.log("GOT NAME ",CAT.products[i].cm_name);
                                       if (typeof CAT.products != "undefined"){
                                          productName = CAT.products[i].cm_name;
                                       }
                                   }
                               }
                           }
                        });
                    }
                }

            }

           self.addPendingTags({"CoreMetrics":{"dom:loaded":[{"params":[cur_prod,productName,cur_cat ,null,null,null],"tag":"cmCreateProductviewTag"}]}});
       

   });
   
   

   //Search hook for frontend and soon Endeca
   document.observe("search:results", function(event){
    var res = event.memo.msg;
    var search_page_id = espanolPrefix + res.pageid;
    self.addPendingTags({"CoreMetrics":{"dom:loaded":[{"params":[search_page_id,res.keywords,res.cat,'"' + res.count +'"',null],"tag":"cmCreatePageviewTag"}]}});
  });


     // BV loaded and ready this is just a place to handle the event, tags come from the backend config.
     document.observe("bv:loaded", function(){
         //console.log("BV IS LOADED",self.jsEventsWaited);
         self.jsEventsWaited.each(function(EVENT){
             if (EVENT.after && EVENT.after == 'bv:loaded'){
                 self._addFrontendEvent(EVENT);
             }
         });
     });

     document.observe("bv:created", function(){
         //console.log("BV IS LOADED",self.jsEventsWaited);
         self.jsEventsWaited.each(function(EVENT){
             if (EVENT.after && EVENT.after == 'bv:created'){
                 self._addFrontendEvent(EVENT);
             }
         });
     });
     
     // site.globalnav.config has loaded event
     document.observe("sitenav:loaded", function(args){
         //console.log("analytics: SITE NAV loaded ",args);
         self.createMap(args.memo.items);
     });


        // our RPC hook
        document.observe('RPC:RESULT', function(obj){
     
            var rpcRequestArray, rpcResponseArray;
            var requestMethod, requestId;
            if (typeof obj.memo.msg.request != "undefined") {
                rpcRequestArray = (obj.memo.msg.request.parameters.JSONRPC != null) ?
                    obj.memo.msg.request.parameters.JSONRPC.evalJSON() :
                    null;

                if (rpcRequestArray) {
                    rpcResponseArray = obj.memo.msg.responseText.evalJSON();
                    if (rpcResponseArray) {
                        rpcRequestArray.each(function(rpcRequest){
                            requestMethod = rpcRequest.method;
                            requestId = rpcRequest.id;
                            // console.log("Analytics handling RPC request:  ", requestMethod, " with id: ", requestId);

                            // We can do special handlers for requests here or filter out non handled requests... 
                            if (!self.RPC_METHODS_ALLOWED.get(requestMethod)) {
                                 //console.log("Analytics skipped ", requestMethod);
                            } else {
                                // Make sure we have the response for this request (id's must match).
                                var myRpcResponse = rpcResponseArray.find(function(rpcResponse){
                                    return rpcResponse.id == requestId
                                });
                                if (myRpcResponse && myRpcResponse.result != null) {
                                    //console.log("Analytics will handle ", myRpcResponse.result.data.Analytics);
                                    var newTags = myRpcResponse.result.data.Analytics;
                                    self.addPendingTags(newTags);
                                }
                            }
                        });
                    }
                }
            }
        });
   

    //Page data already exists hook
        document.observe('PAGEDATA:RESULT', function(obj){
            //console.log("GOT PAGE DATA ",obj);
            if (typeof obj.memo != "undefined"){
                var catalog_path = obj.memo.msg;
                var isLooks=false;
                var localPath=document.location.pathname;
                 
                 if (localPath.match("looks") ){
                     catalog_path="catalog.mpp";
                     isLooks=true;
                 }
                 if (localPath.match("newsworthy") ){
                     return; // skip not an MPP in a taging sense even though it has page_data
                 }
                if (typeof eval ('page_data.' + catalog_path) != "undefined"){
                    if (prodList =  eval ('page_data.' + catalog_path ) ){
                            //console.log("page data from products ", prodList);
                            if (obj.memo.msg.match("mpp") ){
                              for (var i=0; i < prodList.length; i++){
                                if( prodList[i].shaded == 0){
                                 //console.log("pending tags ",prodList[i].cm_name,prodList[i].category_id,prodList[i].product_id,prodList[i].shaded ); 
                                 self.addPendingTags({"CoreMetrics":{"dom:loaded":[{"params":[prodList[i].product_id,prodList[i].cm_name,prodList[i].category_id ,null,null,null],"tag":"cmCreateProductviewTag"}]}});
                                }
                              }
                            }    
                            if ( catalog_path.match("spp") ){
                                //console.log("pending tags SPP"); 
                                //console.log("pending tags ",prodList.cm_name,prodList.category_id,prodList.product_id,prodList.shaded,prodList.cross_sell);
                                self.swatchSkip = true;
                                self.addPendingTags({"CoreMetrics":{"dom:loaded":[{"params":[prodList.product_id,prodList.cm_name,prodList.category_id ,null,null,null],"tag":"cmCreateProductviewTag"}]}});
                                if (prodList.cross_sell.length > 0){
                                    for (var i=0; i < prodList.cross_sell.length; i++){
                                        if (prodList.cross_sell[i].shaded==0){
                                             p = prodList.cross_sell[i];
                                             //console.log("pending tags cross sell ",p.cm_name,p.category_id,p.product_id);
                                            self.addPendingTags({"CoreMetrics":{"dom:loaded":[{"params":[p.product_id,p.cm_name,p.category_id ,null,null,null],"tag":"cmCreateProductviewTag"}]}});

                                        }
                                    }
                                }
                            }
                            if ( isLooks==true ){ // more special cases for some reason
                                //console.log("pending tags Looks");
                                Object.keys(prodList).each(function(CATEGORY){
                                    var CAT = prodList[CATEGORY];
                                    if (typeof CAT != "undefined"){
                                        for (var i=0; i < CAT.products.length; i++){
                                            if (typeof CAT.products[i] != "undefined"){
                                                if( CAT.products[i].shaded == 0){
                                                    //console.log("pending tags looks ",CAT.products[i].cm_name,CAT.products[i].category_id,CAT.products[i].product_id,CAT.products[i].shaded );
                                                    self.addPendingTags({"CoreMetrics":{"dom:loaded":[{"params":[CAT.products[i].product_id,CAT.products[i].cm_name,CAT.products[i].category_id ,null,null,null],"tag":"cmCreateProductviewTag"}]}});
                                                }
                                            }
                                        }
                                    } 
                                    
                           
                                });
                            }
                    }else{
                        //console.log("NO TAG DATA FROM PAGE DATA - catalog_path");
                    }
                }else{
                    //console.log("NO TAG DATA FROM PAGE DATA - catalog_path doesn't exist");
                }
            }else{
                //console.log("NO TAG DATA FROM PAGE DATA - obj.memo undef");
            }

        });

        //Element Tags - For Artists Tools
        // Trigged by brand.product.addButton after rpc call that successfully adds item to bag
        document.observe("cartButton:success", function(resultData) {
        
        var target = resultData.target || {};
        var targetId = target.id || "";

        if ( targetId == "" ) {
            return;
        }

        var prodId = false;

        // SPP id="prod_sku" .. cross sells id="prod_sku-$prodId"
        if (targetId.match("prod_sku-(PROD[0-9]*)")) {
            prodId = targetId.match("prod_sku-(PROD[0-9]*)")[1];
        }

        if (prodId === false) {
            return;
        }

        var catalog_path = "catalog.spp.product";

        if (typeof eval ('page_data.' + catalog_path) != "undefined"){
            if (prodList =  eval ('page_data.' + catalog_path ) ){

                var cross_sell_items = prodList.cross_sell || [];

                var csProduct = cross_sell_items.find(function(cs_prod){
                    return cs_prod.product_id == prodId;
                });

                if (csProduct) {
                    var elementId = csProduct.cm_name; // prod name of the xsell item
                    var elementCat = "Artist Tools - " + prodList.cm_name; // Current product's name
                    //Element Ids and Element Cat Ids cannot exceed 50 Chars
                    var elementId_trim = elementId.substr(0,50);
                    var elementCat_trim = elementCat.substr(0,50);
                    self.addPendingTags({"CoreMetrics":{"dom:loaded":[{"params":[elementId_trim,elementCat_trim,null,null,null,null],"tag":"cmCreatePageElementTag"}]}});
                }
            }

        }
    });

       
     },
    


    addDynamicListener: function(taggingModule, myEvent, TagBlocks){
        var self = this;
        var id = "default";
        TagBlocks.each(function(tagBlock){
            if (typeof tagBlock.memo != "undefined") {
                id = tagBlock.memo;
            }
            if (!self.subscribers[id]) { self.subscribers[id] = {}; }
            if (!self.subscribers[id][taggingModule]) { self.subscribers[id][taggingModule] = {}; };
            if (!self.subscribers[id][taggingModule][myEvent]) {
                self.subscribers[id][taggingModule][myEvent] = new Array();
            } else {
                 //console.log( "Event is defined ", self.subscribers[id][taggingModule][myEvent] );
            }

            if (!self.seenMemoWithTag[id]) { self.seenMemoWithTag[id] = {}; }
            if (!self.seenMemoWithTag[id][tagBlock.tag]) {
                self.seenMemoWithTag[id][tagBlock.tag] = 1;
                self.subscribers[id][taggingModule][myEvent].push(tagBlock);
                //console.log( "pushed Event for: id: ", id, " taggingModule: ", taggingModule, " event: ", myEvent, " -> ", self.subscribers[id][taggingModule][myEvent] );
            }

        });

        // Attach the listener for this event type.
        if (!self.listeners[myEvent]) {
            self.listeners[myEvent] = 1;
            //Event.observe(window, myEvent, function(evt){
            document.observe(myEvent, function(evt){
                //console.log("running window event: ", myEvent, " with memo: ", evt.memo);

                var myId = evt.memo;
                self.enabledModules.each(function(taggingModule){
                     //console.log("for tagging module: ", taggingModule);
                    if (typeof self.subscribers[myId][taggingModule] != "undefined"){
                       if (self.subscribers[myId][taggingModule][myEvent]) {
                             //self.execEventTagBlocks( self.subscribers[myId][taggingModule][myEvent] );
                          if ( taggingModule.match("CoreMetrics") ) {                                    
                              self.execEventTagBlocks( self.subscribers[myId][taggingModule][myEvent] ); 
                          }else{                                                                         
                              self.execImageBlocks(  self.subscribers[myId][taggingModule][myEvent], evt );   
                          }                                                                              

                       }
                    }
                });
            });
        }
    },
    execTags: function (){
        var self = this;
        if (typeof self.pendingTags == "object") {
            Object.keys(self.pendingTags).each(function(taggingModule){
                if (typeof taggingModule != "object" && self.pendingTags[taggingModule] == 'notag') {
                        return;
                }
                Object.keys(self.pendingTags[taggingModule]).each(function(myEvent){
                    // console.log("Analytics: module / event ", taggingModule, myEvent);
			        if (myEvent != 'dom:loaded'){
                        // register this event with our collection of listeners.
                        self.addDynamicListener(taggingModule, myEvent, self.pendingTags[taggingModule][myEvent]);
                        // and then execute it
                        //self.execEventTagBlocks( self.pendingTags[taggingModule][myEvent] );
                    } else {
                        // incoming tagging events under the 'dom:loaded' label can be executed straightaway.
                        //self.execEventTagBlocks( self.pendingTags[taggingModule][myEvent] );
                        if ( taggingModule.match("CoreMetrics") ) {                              
                            self.execEventTagBlocks( self.pendingTags[taggingModule][myEvent] ); 
                        }else{                                                                   
                            self.execImageBlocks( self.pendingTags[taggingModule][myEvent], null );    
                        }                                                                        
                    } 
                }); 
            });
        }
    },
    execEventTagBlocks: function(tagBlocks){
        tagBlocks.each(function(tagBlock){
            if (!tagBlock.params || !tagBlock.tag) { return; }
                 //console.log( "Analytics.execEventTagBlocks about to execute tag: ", tagBlock.tag, " with params: ", tagBlock.params );
            if (typeof window[tagBlock.tag] == "undefined") {
                 //console.log( "The Tagging Module function is not found: ", tagBlock.tag );
                return;
            }
            window[tagBlock.tag].apply(this, tagBlock.params);
        });
    },

/*
    execImageBlocks: function(tagBlocks){                                                                      
        // these often have a rnd number, gen that here to make sure it isn't used over and over as a global   
        var axel = Math.random() + "";                                                                         
        var num = axel * 1000000000000000000;                                                                  
        tagBlocks.each(function(tagBlock){                                                                     
            var tag_url = new Image();                                                                         
            tagBlock = tagBlock.replace("rndnum",num);                                                         
            console.log( "Analytics.execImageBlocks about to load image: ", tagBlock);
            tag_url.src = tagBlock;                                                                            
        });                                                                                                    
    },
*/

    execImageBlocks: function(tagBlocks, evt){

        // If we were called because of a click on an href, save the url, stop the browser from following,
        // and then move to the url after the images have loaded. No url or same page anchor tags?
        // Then we only load the images.
        var href = null;
        Event.extend(evt);
        if (typeof evt.target.href != "undefined") {

            href = evt.target.href;

            var ele = Event.element(evt);
            var elehref = ele ? ele.getAttribute("href") : '';
            if ( elehref.match(/^#/) ) {
                // Ignore links to page anchors
                href = null;
            }

            if (href) {
                //console.log("Stopping browser from following link ", href);
                //evt.preventDefault();
                Event.stop(evt);
            }
        }

        // these often have a rnd number, gen that here to make sure it isn't used over and over as a global
        var axel = Math.random() + "";
        var num = axel * 1000000000000000000;

        var tagImages = new Array();
        
        tagBlocks.each(function(tagBlock){

            // src => 'http://foo/bar'
            // src => [ 'http://foo', 'http://bar' ]
            var imgSources = (typeof tagBlock.src == "string") ? [ tagBlock.src ] : tagBlock.src; 

            imgSources.each(function(src){
                var img = document.createElement('img');

                // onload and friends must be set before setting img.src = ...
                // FF/others will load when you img.src = ... but won't set
                // the onload method until this block finishes
                // Also be weary of IE7.. Each iteration of an animated gif
                // will trigger an onload
                img.hasCompleted = false;
                img.onload = function() { this.hasCompleted = true; };
                img.onerror = function() { this.hasCompleted = true; };
                img.onabort = function() { this.hasCompleted = true; };

                tagImages.push(img);

                src = src.replace("rndnum",num);

                //console.log( "Analytics.execImageBlocks about to load image: ", src);

                img.src = src;
            });
        });

        if (href != null) {
            var testAttempts = 0;
            function testImg(){
                //console.log("Analytics.execImageBlocks checking completed");
                testAttempts++;
                var numCompleted = 0;
                tagImages.each(function(img){
                    //console.log("Checking ", img.src, img);
                    if (img.hasCompleted || (img.complete != null && img.complete == true)) {
                        numCompleted++;
                    }
                });
                if ((numCompleted == tagImages.length) || (testAttempts >= 3)) {
                    //console.log("Completed.");
                    if (href != null) {
                        //console.log("Moving browser to ", href);
                        window.location = href;
                    }
                    return;
                }            
                setTimeout(testImg, 500);
            }
            setTimeout(testImg, 500);
        }
    },


     _addFrontendEvent: function(EVENT){
        var self = this;

        if ( $(EVENT.domID) == null && EVENT.event != "dom:loaded" ){
            //console.log("CM ELEMENT does not EXIST! ",EVENT.domID);
            //return;
        }
        if (EVENT.event == "dom:loaded"){
            self.execTagsbyType(EVENT, null);
        }else{
            if (EVENT.domID || (EVENT.attachAttr && EVENT.attachValue) ){
               self._attachFrontendEvent(EVENT);
            }
        }
    },

    _attachFrontendEvent: function(EVENT){
        var self = this;
            // EVENT.attachAttr && EVENT.attachValue are required unless EVENT.domID is specified(for legacy cases with email signups)

            if (EVENT.domID){
                 EVENT.attachAttr = 'id';
                 EVENT.attachValue = EVENT.domID;
            }
            if (typeof EVENT.attachTag == "undefined"){
                 EVENT.attachTag = '';
            }
            if (EVENT.attachAttr && EVENT.attachValue){
               //console.log ( "got ",EVENT.attachTag,EVENT.attachAttr,EVENT.attachValue,EVENT.event);
               var theseListeners = [];
               $$(''+ EVENT.attachTag + '[' + EVENT.attachAttr + '="' + EVENT.attachValue + '"]').each(function (ele){
                    EVENT.hookIds.push( $(ele).identify() ); // if id doesn't exist give it one to use for matching in event handlers
                    self.fromConfigListeners.push(ele); // keep a full current list for ref
                    theseListeners.push(ele); // but don't dupilicate events
               });

                 theseListeners.each( function(ele){
                       //console.log("DOM ID IS ",EVENT.domID,EVENT,ele);
                     ele.observe(EVENT.event, function(evt){
                        //console.log("running event ",evt);
                        if (self.uniqueEvents.indexOf(ele) != -1){
                            return;                               
                        }                                         
                        self.uniqueEvents.push(ele);              

                        // which events match in the current list?
                        var cevents;
                        self.jsEvents.each(function(CEVENT){
                             CEVENT.hookIds.each(function(hooks){
                                var nodecheck = null;
                                if (hooks == evt.target.parentNode.id){
                                   nodecheck = evt.target.parentNode.id;
                                }
                                if (hooks == evt.target.id){
                                   nodecheck = evt.target.id;
                                }
                                if (nodecheck){
                                    cevents = CEVENT;
                                    //console.log("HOOK MATCHED ",hooks,CEVENT);
                                    // now we know the event and have the data, exec the tag according to type
                                    self.execTagsbyType(CEVENT, evt);
                                }
                             });
                        });
                        ele.stopObserving();
                     });
               });
            }
     },

    // tags specificly connected to user actions and tags more closely connected to frontend events vs backend data, like prodcat
     /*
         cmCreatePageElementTag(elementID, elementCategory,attributes)
         cmCreateManualPageviewTag(pageID, categoryID,DestinationURL,ReferringURL)
         cmCreateManualLinkClickTag(href,name,pageID)
         cmCreateManualImpressionTag(pageID, trackSP, trackRE)
         cmCreateErrorTag(pageID, categoryID)
     */
    execTagsbyType: function(CEVENT, evt){

        // now config can use any JS var as param value.                                                              
        Object.keys(CEVENT).each(function(param){            
                if ( param.match("_") ){                      
                    //console.log("match eval type param");     
                    //console.log ( CEVENT[param]  );           
                    newParam = param.replace(/_/,'');         
                    CEVENT[newParam] = eval( CEVENT[param] ); 
                }                                             
        });                                               

        if (CEVENT.type == 'conversion_event'){
            if (CEVENT.points < 1){
                CEVENT.points = '"0"';
            }
            this.addPendingTags({"CoreMetrics":{"dom:loaded":[{"params":[CEVENT.eventID, CEVENT.actionType, CEVENT.cat, CEVENT.points,CEVENT.attributes],"tag":"cmCreateConversionEventTag"}]}});
        }
        if (CEVENT.type == 'element'){
            this.addPendingTags({"CoreMetrics":{"dom:loaded":[{"params":[CEVENT.elementID,CEVENT.elementCategory,CEVENT.attributes],"tag":"cmCreatePageElementTag"}]}});
        }
        if (CEVENT.type == 'mpageview'){
            this.addPendingTags({"CoreMetrics":{"dom:loaded":[{"params":[CEVENT.pageID,CEVENT.categoryID,CEVENT.DestinationURL,CEVENT.ReferringURL],"tag":"cmCreateManualPageviewTag"}]}});
        }
        if (CEVENT.type == 'mlinkclick'){
            this.addPendingTags({"CoreMetrics":{"dom:loaded":[{"params":[CEVENT.href,CEVENT.name,CEVENT.pageID],"tag":"cmCreateManualLinkClickTag"}]}});
        }
        if (CEVENT.type == 'mimpression'){
            this.addPendingTags({"CoreMetrics":{"dom:loaded":[{"params":[CEVENT.pageID,CEVENT.trackSP,CEVENT.trackRE],"tag":"cmCreateManualImpressionTag"}]}});
        }
        if (CEVENT.type == 'error'){
            this.addPendingTags({"CoreMetrics":{"dom:loaded":[{"params":[CEVENT.pageID,CEVENT.categoryID],"tag":"cmCreateErrorTag"}]}});
        }
        if (CEVENT.type == 'img'){              
           //console.log("IMG tracker",CEVENT);   
           this.execImageBlocks([CEVENT], evt); 
        }                                       

    },


    // Possible special funtions for front end (reserved for future use)
    onDivShow: function() {},
    onFrameUpdate: function() {},
    onJsRedirect: function() {},





// More just for Mac, but the createMap func itself or something like it could become more standard for Navs. 
createMap: function(args) {
    //console.log("MAP BUILD");

    //var sections = site.globalnav.config.items;
    var sections = args;

    for (x in sections) {
        for (y in sections[x]){
            if (y === "items"){
                var items = sections[x].items;
                for (var element = 0; element < items.length; element++){
                    if (items[element] != null){
                        if (typeof items[element] != "undefined" && typeof(items[element].id)  != "undefined" && typeof(items[element].cmcat) != "undefined"){
                            this.cm_map[items[element].id] = items[element].name;
                        }
                        if ( !(items[element].id.match("CAT[0-9]+")) ){
                            this.cm_corrected_cat[items[element].id] = items[element].cmcat;
                        } 
                    }
                 }
             }else{
                 if (typeof(sections[x].id)  != "undefined" && typeof(sections[x].cmcat) != "undefined"){
                     if ( !(sections[x].id.match("CAT[0-9]+")) ){
                         this.cm_corrected_cat[sections[x].id] = sections[x].cmcat;
                     }else{
                         this.cm_map[sections[x].id] = sections[x].name;
                     }
                 }
            }
       }
    }
}


});
Analytics = new Analytics();

/*
 * File: brand.globalnav.GlobalNav
 * Auth: Steph Shindler & Mina Kumar
 */
                                         
/*

Communication up/down hierarchy:
    - GlobalSet: onChildClick called from children: Accordion
    - PanelNavSet: onChildClick called from children: PanelSubNav
    - PanelNavSet & GlobalSet: setActiveItem called from children: PanelNav._setActive (sets ActiveItem for PanelNavSet & GlobalSet AND SETS Accordion.ActiveSubItem)
    - PanelSubNav/ProductSubNav: onParentClick called from parent: PanelNav


1st LEVEL:
-----------------------
2 Accordions & 2 Panel Navs:
    - gnav_products, gnav_artistry
    - pnav_givingback, pnav_mymac ("pnav" instead of "gnav" because these are PanelNav trigger/headers, so their panel gets named after them; pnav_givingback_panel

2nd LEVEL (GENERAL): 
-----------------------
This level can either be a single PanelNav or multiple PanelNavs owned by a PanelNavSet:
    - Option A: 1 PanelNavSet + 1 Panel
        - multiple PanelNavs
    - Option B: 1 PanelNav + 1 Panel
    
Attachment notes:
    - PanelNavSet gets instantiated as a child of the Accordion above it.
    - PanelNav attachment requires 2 things: 
        (1) place the panelnav header/trigger node in the correct place in the DOM = pass PanelNav node to Accordion's addSubItem method which does a DOM insertion
        (2) place the panelnav widget object in its place in the widget hierarchy = pass parentId prop on instantiation which sets widget parent
    - Panels get created by either PanelNavSet._addPanel (if Panel is associated with a set of PanelNavs) or PanelNav._addPanel (if Panel is associated w/ only 1 PanelNav)

3rd LEVEL (GENERAL): 
-----------------------
Each PanelNav (2nd level) has 1 PanelSubNav or an extension of PanelSubNav:
    - PanelSubNav: Div container for panel content associated w/ a single PanelNav link
    - ProductSubNav: container template that includes nodes for ProductCategoryDetail & a category Accordion     
    
*/ 

brand.globalnav = { abort: false };

brand.globalnav.GlobalNav = Class.create(
{
    // summary:
    //      Iterates through site.loader.config hierarchy and applies correct classes
    //      to each level of the nav tree (section, super-category, category and detail/product). 
    //        Compares data with page_data.panel_nav to check for default state of the nav.
    
    config: null,
    _configKeys: {},
    defaultState: {}, // item hierarchy of default panel
    defaultNavCreated: false,
    globalNavSetId: "",

    initialize: function(args) {
        this.config = args.config;
        this.defaultState = args.defaultState;
        this.globalNavSetId = args.globalNavSetId; 
        var self = this;
        
        // fired from panelManager._onClick
        generic.events.observe("panelnav:click", function(event) {
            self.getPanelContent(event);
        }); 
        
        // start loading: iterates through config hierarchy
        this.initSections();
    },
    
    initSections: function() {
        // summary:
        //      Iterates through top-level sections (as passed from config)
       
        var sections = this.config.items;
        var defaultSection = this.defaultState.id;
        var self = this; 
        
        sections.each(function(section, idx) {
            if (!section) return;
            // check for default section
            var isDefaultSection = false; // reset for every loop instance
            if (section.id === defaultSection) {
                isDefaultSection = true;
            }
            self._configKeys[section.id] = { idx: idx, items: {} };
                        
            // section has a set of panel navs
            if (section.items && (section.items.length > 0)) {
                self.initPanelNavSet(section, isDefaultSection);

            // item is a direct link from left nav
            } else if (section.uri && !section.content) {
                self.initHeader({type: "pnav", item: section, isdefault: isDefaultSection, parentId: self.globalNavSetId, domParent: section.domNode, domParentType: "GlobalSet"});
                
            // section has only 1 panel nav
            } else {                
                if (section.id === "search") {
                    self.initSearch(section);
                    
                } else {                  
                    // console.log("GlobalNav.initSections -> initPanelNav");
                    self.initPanelNav({item: section, isdefault: isDefaultSection, domParentType: "GlobalSet"});
                }
            }
                    
        });
    
    },
    
    initPanelNavSet: function(section, isDefaultSection) {  
        
        // summary:
        //      1 Accordion + PanelNavSet to 1 Panel with N num of PanelNavs (i.e. Products)
        //      Displays (a set of) PanelNav links in an Accordion container as top-level/left-hand links
 
        // init Accordion for set
        var accordionContainer = new site.globalnav.Accordion({
            id: "gnav_"+section.id, 
            parentId: section.parentId, 
            domParent: section.domParent,
            pnavsetChild: "pnavset_"+section.id,
            displayName: section.name,
            hdPath: section.header  
        });

        // init PanelNavSet
        var pnavset = new brand.globalnav.PanelNavSet({
            id: "pnavset_"+section.id, 
            parentId: "gnav_"+section.id
        });

        var defaultPnav = this.defaultState.item;
        var defaultPnavId = (defaultPnav ? defaultPnav.id : null);
       
        var self = this;
        // init PanelNavs for this PanelNavSet
        section.items.each(function(pnavItem, idx) {
            if (!pnavItem) return;
            self._configKeys[section.id].items[pnavItem.id] = { idx: idx };
            self.initPanelNav({item: pnavItem, defaultId: defaultPnavId, section: section, domParent: accordionContainer, domParentType: "PanelNavSet"});
        });

        // now that all the left-hand supercategories have been populated, set the state of the gnav accordion
        if (isDefaultSection) {
            accordionContainer.open();
        }
           
    },

    initPanelNav: function(args) {
        var pnavItem = args.item;
        var section = (args.section ? args.section : pnavItem); // if no parent section, this item is a top-level section
        var domParent, pnavDomParent, parentId, headerParentId;

        // get parent child context for top-level vs. second level panel navs
        if (args.domParentType === "GlobalSet") { // top-level item (e.g. giving_back)
            domParent = pnavDomParent = pnavItem.domParent;
            parentId = pnavItem.parentId;
        } else if (args.domParentType === "PanelNavSet") { // second-level item (e.g. whats new) parent is a PanelNavSet/Accordion
            domParent = args.domParent;
            pnavDomParent = domParent.containerNode;
            parentId = "pnavset_" + section.id;
        }
        
        // isDefaultPanel: is PanelSubNav instance associated w/ default category
        var isDefaultPanel = false;
        if (args.isdefault || (!this.defaultNavCreated && (pnavItem.id === args.defaultId))) {
            isDefaultPanel = true;
        }

        // default header args: for regular/href link or isDefaultPanel
        var headerArgs = {type: "pnav", item: pnavItem, isdefault: isDefaultPanel, parentId: parentId, domParent: domParent, domParentType: args.domParentType}
        
        // find cases of items with panel nav content but suppressed gnav links (e.g. studio)
        var showGnavLink = (!pnavItem.suppressGnavLink ? true : false);
        var createPanelNav = ((showGnavLink || isDefaultPanel) ? true : false);
        
        // item is an href link from left nav
        if (pnavItem.uri && !pnavItem.content && showGnavLink) {
            this.initHeader(headerArgs);
        
        //  item has a panel to populate w/ content
        } else {
            
            if (showGnavLink) {
                if (isDefaultPanel) {
                    this.initHeader(headerArgs);
                    
                } else { 
                    //console.log("GlobalNav.initPanelNavSet: create PanelNav "+ "pnav_"+pnavItem.id + " / " +domParent.id);
                    //create PanelNav for the set
                    var pnav = new brand.globalnav.PanelNav({
                        id: "pnav_"+pnavItem.id, 
                        parentId: parentId, // widget parent *NOT* DOM parent
                        domParent: pnavDomParent, // DOM parent 
                        displayName: pnavItem.name,
                        hdPath: pnavItem.header,
                        sectionId: section.id,
                        item: pnavItem
                    });               
                    // now that pnav has a place in the DOM, start it up:
                    pnav.startup(); 
                }
            }
          
            // continue to subnav level
            if (createPanelNav) {
                this.initPanelSubNav(pnavItem, isDefaultPanel, section);
            }

        }
    },    
            
    initPanelSubNav: function(pnavItem, isDefaultPanel, section) { 
        //console.log("GlobalNav.initPanelSubNav "); 
        
        // summary:
        //      Creates each instance of either a PanelSubNav or ProductSubNav
        //      associated with parent PanelNav
        
        var hasLoaded = false;

        // default PanelSubNav args
        var psubnavArgs = {
            id: "psubnav_"+pnavItem.id,
            parentId: "pnav_"+pnavItem.id,  
            isDefaultPanel: isDefaultPanel,
            itemId: pnavItem.id
        }
        // get config for item
        // content/widget config settings will either be at item level (i.e. whats new) or at parent level (i.e. supercategories like eyes have shared settings as a group)
        if (pnavItem.content) {
            psubnavArgs.content = pnavItem.content; // individual level settings
        } else if (section.itemsConfig) {
            psubnavArgs.content = section.itemsConfig.content; // has shared settings
        }
        
        if (pnavItem.hasLoaded) psubnavArgs.hasLoaded = true;
 
        if (pnavItem.id === "discontinued") {
            psubnavArgs.content = pnavItem.content;
            psubnavArgs.templatePath = "jsTemplates.globalnav.DiscontinuedSubNav";
            var psubnav = new brand.globalnav.PanelSubNav(psubnavArgs);
            
        // prodcat sub-sections & looks
        } else if (psubnavArgs.content && psubnavArgs.content.widget === "ProductSubNav") {
            //console.log("GlobalNav.PanelSubNav - prodcat sub-sections & looks"); 
            // create ProductSubNav (extension of PanelSubNav) for the PanelNav
            var psubnav = new site.globalnav.ProductSubNav(psubnavArgs);

        // cms-generated & other nav types
        } else {
            //console.log("GlobalNav.PanelSubNav - cms-generated & other nav types : "+pnavItem.id);          
            var content = pnavItem.content;
            var isCMS = false;
            var isDefaultCMS = false;

            // get psubnav class to use
            var psubnavClass = "PanelSubNav"; // use PanelSubNav by default
            if (content && content.widget) {
                psubnavClass = content.widget;
            }

            // global nav tmp writes out cms html navs for default state (non-prod cat only)
            if (content && content.cms && content.handleAs === "html") {
                //console.log("GlobalNav.PanelSubNav - global nav tmp writes out cms html navs");  
             
                hasLoaded = true;
                isCMS = true;
                
                if (isDefaultPanel) {
                    isDefaultCMS = true;                    
                    // set rollovers & default/detail states in html/cms nav content
                    this.initCMSDisplay({isdefault: isDefaultCMS});
                } 
            }
            
            if (!isDefaultCMS) { 
                //console.log("GlobalNav.PanelSubNav - !isDefaultCMS : psubnavArgs = ",psubnavArgs); 
                psubnavArgs.content = content;
                var psubnav = new site.globalnav[psubnavClass](psubnavArgs);                
            }
        }

        // render default panel content
        if (isDefaultPanel && !hasLoaded) {
            //console.log("GlobalNav.PanelSubNav - render default content for "+pnavItem.id); 
            this.getPanelContent({psubnav: psubnav, sectionId: section.id, item: pnavItem});
        }
    },
    
    getPanelContent: function(args) { 
        //console.log("GlobalNav.getPanelContent "); 
        // summary:
        //      Handles request for file (as specified in config) containing content
        //      for panel (PanelSubNav) associated with a given left-hand nav link

        var psubnav = args.psubnav;
        var self = this;
        if (psubnav.id === "psubnav_account") {
            $(psubnav.id).removeClassName("hidden");
            generic.events.fire({event:"globalnav:getcontent/my_mac"});  
        } else {
            //var url = "/js/brand/globalnav/data/" + psubnav.itemId + ".txt"; // for testing
            var url = psubnav.content.url + (psubnav.content.param ? ("?" + psubnav.content.param + "=" + psubnav.itemId) : "");
             
            new Ajax.Request(url, {
                method: 'get',
                onSuccess:  function(transport) { 
                    var data = transport.responseText;
                    this.initPanelContent(data, args); 
                }.bind(this) 
            }); 
        }
        
    },
    
    initPanelContent: function(data, panelArgs) {
        // summary:
        //      Handles response (as html or json) from getPanelContent
        //console.log("GlobalNav.initPanelContent ");   
       
        var psubnav = panelArgs.psubnav; 
        // place html content
        if (psubnav.content.handleAs === "html") {            
            psubnav.addSubItem(data);
            
            // set rollovers in html/cms nav content
            if (psubnav.content.cms) {  
                this.initCMSDisplay({scopeNode: psubnav.id});
            }
        
        // loop thru json data to render content variations
        } else {                               
            var sectionId = panelArgs.sectionId;
            var pnavItemId = (panelArgs.item ? panelArgs.item.id : panelArgs.itemId);
            //console.log("GlobalNav.initPanelContent: json "+ sectionId +  "/" + pnavItemId);               
                                     
            // response will either be an array of items or an object that contains the item array further down the tree
            if (typeof data === "string") data = data.evalJSON(true); 
            var items = data; // default
            
            // broadcaster
            generic.events.fire({event:"panel:prodcat", msg:{ data: data, request: psubnav}});

            if (typeof data === "object") {
                // ex: prodcat data
                if (data.sections) {
                    items = data.sections[0].items;
                
                // ex: faves (SectionDescSubNav)
                } else if (data.items) {
                    items = data.items;
                    // pass additional data props (above items)
                    if (typeof psubnav.setContent === "function") {
                        psubnav.setContent(data);
                    }
                }
            }
 
            // get content hierarchy type
            var useProductCategories = false; // flag true if prod content has 2-levels deep (categories & detail items) 
            if (pnavItemId !== "discontinued" && (sectionId === "products" || pnavItemId === "looks")) {        
                useProductCategories = items.any(function(item) { 
                    if (item.items) return true; 
                });   
                //console.log("GlobalNav.initPanelContent: useProductCategories "+useProductCategories); 
            }

            // prodcat special case: discontinued nav 
            if (pnavItemId === "discontinued") {
               //console.log("GlobalNav.initPanelContent: discontinued");  
               this.initDiscontinued(items, psubnav, panelArgs.item);
           
            //prodcat: if item hierarchy goes 2-levels deep
            } else if (useProductCategories) {            
                //console.log("GlobalNav.initPanelContent: prodcat: if item hierarchy goes 2-levels deep");   
                this.initProductCategories(items, psubnav, sectionId, pnavItemId);
               
            //prodcat & other sections w/ only 1 level deep
            } else { 
                 //console.log("GlobalNav.initPanelContent: prodcat & other sections w/ only 1 level");   
                // if no section defined, item is the section
                if (sectionId === "") {
                    sectionId = pnavItemId;
                } 
                
                // get default context
                var hasItemInDefaultCategory = false; // flag true if in default category & category contains default detail item 
                if (psubnav.isDefaultPanel) {
                    
                    // default detail item
                    var defaultDetail = this.getDefaultDetail(); 
                    hasItemInDefaultCategory = items.any(function(detailItem) { 
                        return (!!(detailItem.id === defaultDetail.id));
                    })
                    //console.log("GlobalNav.initPanelContent: hasItemInDefaultCategory "+hasItemInDefaultCategory);    
                    // PanelSubNav
                    if (hasItemInDefaultCategory) {
                        psubnav.setDefaultState();
                    }
                }                
 
                var self = this; 
                // check for specific method to override generic.widget class default DOM insertion (insertMixIn)
                // example: brand.globalnav.ArtistryInActionSubNav uses it's own addSubItem method
                var domInsertionMethod = null;
                if (psubnav.domInsertionMethodName && psubnav[psubnav.domInsertionMethodName]) {
                    domInsertionMethod = function(detail) {
                        psubnav[psubnav.domInsertionMethodName](detail.domNode);
                    }
                };
                
                items.each(function(detailItem, idx) {   
                    //console.log("GlobalNav.initPanelContent: items"+idx + ": "+detailItem.id+" / "+psubnav.id);    
                    // check for header links or detail modules
                    if (detailItem.type === "header_link") {
                        var isdefaultDetail = false;
                        if (defaultDetail && (detailItem.id === defaultDetail.id)) {
                            isdefaultDetail = true;
                        }
                        self.initHeader({type: "psubitem", item: detailItem, isdefault: isdefaultDetail, parentId: psubnav.id, domParentType: "PanelSubNav"});
                    
                    // detail module
                    } else {
                        var detail = self.initDetail({
                            item: detailItem,
                            sectionId: sectionId,
                            pnavItemId: pnavItemId,
                            defaultItem: defaultDetail,
                            isInDefaultCategory: hasItemInDefaultCategory,
                            parentId: psubnav.id,
                            domParentId: psubnav.containerNode,
                            domInsertionMethod: domInsertionMethod
                        }); 
                    } 
                });                
                
            }
            
        } //end loop through json
          
        // hide progress, show content 
        psubnav.onChildrenLoaded({ hasLoaded: true });
        // broadcaster for external hooks
        generic.events.fire({event:"panelnav:contentloaded", msg:{ psubnavId: psubnav.id, itemId: psubnav.itemId,  sectionId: panelArgs.sectionId }});
    },

    initProductCategories: function(supercatItems, psubnav, sectionId, pnavItemId) {
        // summary:
        //      Adds category and product level widgets that render category-detail modules
        //      (ProductCategoryDetail), category accordion headers (Accordion) and product-detail
        //      modules (Detail).
        //      These instances are attached to their parent instance of ProductSubNav
        //console.log("GlobalNav.initProductCategories: "+supercatItems.length); 
        
        var self = this;
        var defaultSupercat = this.defaultState.item;
        var defaultCat = (defaultSupercat ? defaultSupercat.item : null);
        var defaultCatId = (defaultCat ? defaultCat.id : null);
        var useAccordionMode = false; // flag true shows accordion headers as initial state (rather than category detail modules). used for default state of spp's & cat-level mpp's
        var hasMixedDetail = false; // flag true if category level includes non-category (childless) items.  Ex: brush-play detail link in Brushes category list

        // default detail
        if (psubnav.isDefaultPanel && defaultCat) { 
            var defaultDetail;
            try {
                defaultDetail = defaultCat.item;
            }
            catch (err) { }
        }
     
        supercatItems.each(function(catItem, catidx) {
            //console.log("GlobalNav.initProductCategories supercatItems "+catidx+": "+  catItem.id);
            var hasChildren = true; // flag true when item is a category w/ child detail items (i.e. will display as accordion group)
            
            if (!catItem.items) {
                //console.log("GlobalNav.initProductCategories id: "+catItem.id + " has no children");
                hasChildren = false;
                hasMixedDetail = true;
            }
            
            // get default context            
            var isDefaultCat = false; // default category w/ in default ProductSubNav
            var hasItemInDefaultCategory = false; // flag true if category contains default detail item
            // nav is rendering default state of psubnav and has levels beyond supercat
            if (psubnav.isDefaultPanel && defaultCat) {            
                useAccordionMode = true;             

                // verify that category contains a default detail
                // (usually true. false ex: mpp)
                if (catItem.id == defaultCatId) {
                    isDefaultCat = true;
                    if (defaultDetail) {
                        hasItemInDefaultCategory = true;
                    }
                }
            }
            
            // add detail module for item
            if (!useAccordionMode || !hasChildren) { 
                var dargs = {
                    id: "psubcat_"+catItem.id,   
                    displayName: catItem.name,
                    hdPath: catItem.header,
                    description: catItem.description,
                    thumbPath: catItem.thumbnail,
                    sectionId: sectionId,
                    pnavItemId: pnavItemId,
                    parentId: psubnav.id
                }
                //console.log("GlobalNav.initProductCategories "+dargs.id+" / " +psubnav.id);
                
                // add detail link
                if (!hasChildren) {
                    //console.log("GlobalNav.initProductCategories: !hasChildren"); 
                    dargs.item = catItem;
                    dargs.isInDefaultCategory = hasItemInDefaultCategory; 
                    dargs.domInsertionMethod = function(d) {
                        psubnav.addSubItem(d.domNode, psubnav.detailLinksContainerNode);
                    }
                    
                    var detail = self.initDetail(dargs);
                    return; 
                
                // add category detail
                } else if (!useAccordionMode) {
                    //console.log("GlobalNav.initProductCategories - !useAccordionMode "+dargs); 
                    var altDetailConfig = self.getAltTemplateConfig(catItem);
                    if (altDetailConfig) {
                        dargs.template = dargs.template || {} ;
                        dargs.template.detail = dargs.template.detail || {} ;
                        dargs.template.detail = Object.extend(dargs.template.detail, altDetailConfig);
                    } 
                    
                    psubnav.addCategoryDetail(dargs); 
                } 
            }
            
            //var img = catItem.header.replace(/200/g, "250");
            var subnavAccordion = psubnav.addCategoryAccordion({
                id:"psubcat_"+catItem.id,
                displayName: catItem.name,
                hdPath: catItem.header,
                description: catItem.description
            });                     
            
            catItem.items.each(function(detailItem, idx) {
                //console.log("GlobalNav.initP catItems "+idx+": "+ detailItem.id + "/" + subnavAccordion.id);
                 /*
                Detail:
                    - creates product-level detail html module
                
                Attachment notes:
                    Detail node gets passed to Accordion's addSubItem method 
                */
                
                var detail = self.initDetail({
                    item: detailItem,
                    sectionId: sectionId,
                    pnavItemId: pnavItemId,
                    defaultItem: defaultDetail,
                    isInDefaultCategory: hasItemInDefaultCategory,
                    parentId: psubnav.id,
                    domParent: subnavAccordion.containerNode
                });                
               
            }); // end: catItems foreach
                               
            // accordion has items, so toggle its display for default category state
            psubnav.setCategoryState({
                accordion: subnavAccordion,
                useAccordionMode: useAccordionMode,
                isDefaultCat: isDefaultCat, 
                hasItemInDefaultCategory: hasItemInDefaultCategory
            });
            
        });
      
        // psubnav content loaded, so toggle display for default panel state
        psubnav.setPanelState({ hasMixed: hasMixedDetail });
     
    },
   
    initDetail: function(args) { 
        // summary:
        //      Instantiates appropriate Detail widget class and properties according to
        //      content type. 
        
        // args
        var item = args.item;
        var config = brand.globalnav.config;
        var sectionId = args.sectionId;
        var pnavItemId = args.pnavItemId;  
        //console.log("GlobalNav.initDetail " +args.item.id + " / " + args.parentId + " / " +args.pnavItemId); 
        
        // get config if needed
        var sk = (sectionId ? this._configKeys[sectionId] : null);
        var sectionConfig = (sk ? this.config.items[sk.idx] : null);
        var sharedItemConfig = (sectionConfig ? sectionConfig.itemsConfig : null); 
        var pnk = (sectionConfig ? sk.items[pnavItemId] : null);
        var pnavItemConfig = (pnk ? sectionConfig.items[pnk.idx] : sectionConfig);

        // set if default
        var isInDefaultCategory = (args.isInDefaultCategory ? args.isInDefaultCategory : false);         
        var isdefault = false;
        if (isInDefaultCategory) {
            if (args.isdefault) {
                isdefault = args.isdefault;
            } else {
                var defaultId = (args.defaultItem ? args.defaultItem.id : null);
                if (item.id == defaultId) {
                    isdefault = true;
                }
            }
        }
        
        var itemName = (item.name ? item.name : "");
 
        // parameters that always get passed to detail-level widgets
        var wArgs = {
            id: "psubitem_" + item.id,
            displayName: itemName,
            hdPath: item.header,
            //hdHeight: item.header_image_height,
            thumbPath: item.thumbnail,
            thumbRolloverPath: item.thumbnail_rollover,
            url: item.uri,
            isdefault: isdefault,
            parentId: args.parentId,
            isInDefaultCategory: isInDefaultCategory
        }
        if (args.domParent) wArgs.domParent = args.domParent;
        if (args.domInsertionMethod) wArgs.domInsertionMethod = args.domInsertionMethod;
            
        // optional parameters
        // description:
        wArgs.description = (item.description ? item.description : null);
        // template:
        var t;
        if (args.template) {
            t = args.template;
        } else if (pnavItemConfig && pnavItemConfig.template) {
            t = pnavItemConfig.template;
        } else if (sharedItemConfig && sharedItemConfig.template) {
            t = sharedItemConfig.template;
        }

        if (t) {
            wArgs.template = t;
        }
        
        //console.log("GlobalNav.initDetail: add " +wArgs.id + " / " + wArgs.parentId); 
         
        // check for alternate "type" property: defines display styles for individual items that may differ from group
        var altDetailConfig = this.getAltTemplateConfig(item);
        if (altDetailConfig) {
            wArgs.template = ( wArgs.template ? wArgs.template : {} );
            wArgs.template.detail = ( wArgs.template.detail ? wArgs.template.detail : {} );
            Object.extend(wArgs.template.detail, altDetailConfig);
        }       
           
        var detail = new brand.globalnav.Detail(wArgs);
        return detail;
    },

    getAltTemplateConfig: function(item) {
        var dconfig = false;
        if (item.type) {
            var altTypes = this.config.altTypes;        
            var ait = altTypes[item.type];
            
            // if alt type is defined, pass it in template props
            if (ait && ait.detail) {  
                dconfig = ait.detail;
            }
        }        
        return dconfig;
    },
     
    initHeader: function(args) {    
        // summary:
        //      Creates instance of Header for items that don't open a panel or accordion
        //      (including default state of a PanelNav link)
        //      args: type, item, isdefault, parentId, domParent, domParentType
        
        var item = args.item;
        if (!args.item)  return;
        var domParentType = args.domParentType;
        // set contextual args
        var parentId, domParent;
        // get parent child context for top-level vs. second level panel navs
        if (domParentType === "GlobalSet") { // top-level item (e.g. giving_back)
            parentId = args.parentId;
            domParent = args.domParent;
        } else if (domParentType === "PanelNavSet") { // second-level item (e.g. whats new) parent is a PanelNavSet/Accordion
            parentId = args.domParent.id;
        } else if (domParentType === "PanelSubNav") { // e.g. gift cards
            parentId = args.parentId;
        } else {
            console.log("GlobalNav.initHeader : need context handling for id = "+item.id+" / parent type = "+domParentType);
        }
     
        var h = new brand.globalnav.Header({
            id: args.type+"_"+item.id,
            displayName: item.name,
            hdPath: (item.header ? item.header : ""),
            url: (item.uri ? item.uri : null),
            isdefault: args.isdefault,
            parentId: parentId,
            domParent: domParent
        });
    },

    searchError: function(sectionConfig, event) {
        
    },

    initSearch: function(sectionConfig) {
        // summary:
        //      
        
        $('utilitynav_form_search').observe("submit", function (sectionConfig, event) {
            var defaultState = page_data.panel_nav["default"];
            var query = $(sectionConfig.search.formFieldId).value;
            
            if ( !query || query === defaultState.searchDefault ) {
                var popid = sectionConfig.search.errorPopup;
                if (popid) {
                    brand.overlay.launch({
                        foregroundNode: $(popid),
                        displayInline: true,
                        removeOnHide: false,
                        displayDuration: 5000
                    }); 
                }
                
                event.stop();
            }
        }.curry(sectionConfig));
                
        //short circuiting for Endeca
        return;
        
        
        
        var id = sectionConfig.id;
        var self = this; 
        
        // check for default
        var isDefaultPanel = false;
        if (this.defaultState.query && (this.defaultState.item.id === id)) {
            isDefaultPanel = true;
        }
        
        if (!isDefaultPanel) {
            var pnavManager = new brand.globalnav.panelManager({
                id: "pnav_" + id, 
                parentId: this.globalNavSetId,
                sectionId: id,
                item: sectionConfig
            });
            
            pnavManager.startup();
        }
     
        var psubnav = new brand.globalnav.PanelSubNav({
            templatePath: "jsTemplates.globalnav.SearchSubNav",
            id: "psubnav_" + id,
            parentId: "pnav_" + id,
            isDefaultPanel: isDefaultPanel,
            itemId: id,
            cache: false,
            callback: function() {
                var search = new brand.search({
                    config: sectionConfig,
                    panelManagerId: "pnav_" + id,
                    parentId: self.globalNavSetId,
                    isDefaultPanel: isDefaultPanel
                });
            }
        }); 
    },
    
    initDiscontinued: function(items, psubnav, sectionConfig) {  
        //console.log("GlobalNav.initDiscontinued ");  
        var self = this;
          
        // populate DiscontinuedSubNav.html-specific nodes
        if (psubnav.panelDescriptionNode) psubnav.panelDescriptionNode.innerHTML = items.panel_description;
        if (psubnav.searchDescriptionNode) psubnav.searchDescriptionNode.innerHTML = items.description;
      
        // featured goodbyes detail link
        var defaultDetail = this.getDefaultDetail();
        var isdefaultDetail = (defaultDetail.id === "featured_goodbyes" ? true : false);
        var featured = sectionConfig.content.featured;
        featured.description = items.featured_description;
     
        var detail = self.initDetail({
            item: featured,
            template: sectionConfig.template,
            isdefault: isdefaultDetail,
            isInDefaultCategory: isdefaultDetail,
            parentId: psubnav.id,
            domInsertionMethod: function(detail) {
                psubnav.addSubItem(detail.domNode, psubnav.featuredNode);
            }
        });
                        
        // show subnav content
        psubnav.onChildrenLoaded();
 
        if (sectionConfig.search) { 
            var dsearch = new brand.discontinuedSearch({
                config: sectionConfig,
                isDefaultPanel: psubnav.isDefaultPanel,
                progressNode: $("disc_search_progress")
            }); 
        }
        
    },
    
    getDefaultDetail: function(args) {
        // summary:
        //
        //      Find terminus/detail item in page_data.panel_nav.default
        //      args.includeParent: returns object starting with parent of terminus
        //console.log("GlobalNav.getDefaultDetail");
        var d = p = this.defaultState;
        var defaultSubsection = this.defaultState.item; // supercat-level for prodcat.  sub-section level for non-prodcat
        var includeParent = ((args && args.includeParent) ? args.includeParent : false);
        if (defaultSubsection) {
            // step down
            if (defaultSubsection.item) {
                d = defaultSubsection.item; // (ex: products - nails - spp)
                p = defaultSubsection;
                if (d.item) {
                    p = d;
                    d = d.item; // (ex: products - eyes - primer - spp)
                }
                
            // at detail level (ex: giving back - back to mac)
            } else {
                d = defaultSubsection;
            }
        }
        
        return (includeParent ? {detail: d, parent: p} : d);
    },
        
    initCMSDisplay: function(args) {
        // summary:
        //
        //      Sets rollovers for linked images
        //      If is default nav: finds image header in cms nav that matches id
        //      of current page.  Sets image to "on" state

        var scopeNode = (args.scopeNode ? args.scopeNode : "panel_open");
        var defaultImgId = null;
        var rollover;
        var defaultImg; 

        // find image associated with default page state
        if (args.isdefault) {        
            var d = this.getDefaultDetail({ includeParent: true });                        
            defaultImgId = "image_" + d.detail.id;
            // if default image isn't found, check for match in parent section id, or as "_index"
            if (!$(defaultImgId)) {
                var pId = "image_" + d.parent.id;
                if ($(pId)) {
                    defaultImgId = "image_" + d.parent.id;
                    
                // if detail item is terminus item try "_index"
                } else if (!d.detail.item) {
                    pId = "image_index";
                    if ($(pId)) defaultImgId = "image_index";
                }
            }
        } 
        
       // find images in specified nav, apply rollovers
       var imgs = $$("#" + scopeNode + " a img");
       imgs.each(
            function(imgn) {
                if (imgn.id === defaultImgId) {
                    defaultImg = new brand.img(imgn, ["off","on"]);
                    defaultImg.changeSrc("on");
                } else {
                   rollover = new brand.rollover(imgn, null);
                }
            }
        );
    
    }
    
});

 

brand.globalnav.GlobalSet = Class.create(Widget, 
{
    // summary:
    //      Container for set of nav items in left-nav. 
    //      Items/children are mixed widget types (Accordion, PanelNav and Header) 
    //      Children communicate about state changes with each other via the NavSet parent
    
    // activeItemId: String
    // currently active/open child widget id
    activeItemId: "",
     
    // _objChildren: Object
    // storage for children not gettable/settable via other methods
    _objChildren: {},
    
    initialize: function($super, properties) {
    $super(properties);     
    },
    
    addChild: function(child) {
        // summary:
        //      Stores child instances of objects not gettable/settable via other methods
        //      Ex: non-dijit brand.globalnav.panelManager
        
        this._objChildren[child.id] = child;  
    },
    
    getChild: function(childId) {
        // summary:
        //      Returns objects stored via addChild
        //console.log("brand.globalnav.GlobalSet.getChild: "+childId); 
        var child = false;
        var childCount = this.children.length;
        for (var i=0; i<childCount; i++) {
            if (this.children[i].id == childId) {
                child = this.children[i];
                break;
            }
        }  
        return child;
    },
 
    setActiveItem: function(/* String */itemId) {
        this.activeItemId = itemId;
    },

    onChildClick: function(/* String */sectionId,/* Boolean */fromDefault) {
        // summary:
        //      Called by child widgets
        //      Captures events occuring further down in navigation hierarchy
        //      If change is being triggered by default link, closes activeItem, but keeps
        //      default accordion 
        //console.log("GlobalSet.onChildClick "+this.activeItemId +" / "+ sectionId  +" / "+  fromDefault)
        if (this.activeItemId && (this.activeItemId !== sectionId || fromDefault)) {
        
            // get dijit
            var activeItem = $(this.activeItemId).widget;
            // otherwise, look for non-dijit child
            if (!activeItem) {
                try {
                    var gset = $(globalNavSetId).widget;
                    activeItem = this.getChild(this.activeItemId);
                }
                catch(err){};
            }
            
            var defaultId = (fromDefault ? sectionId : ""); 
            activeItem.close(defaultId);
            
        } 
    }
    
});
 
brand.globalnav.Accordion = Class.create( Widget, 
{
    // summary:
    //      Container for a set of nav links where only 1 nav category link's content is visible at a time
    //      Switching between nav categories slides the sub nav links in each nav category up/down 
    
    //templatePath: "/js/brand/globalnav/templates/Accordion.html",  
    templatePath: "jsTemplates.globalnav.Accordion",
    
    templateType: "",
    isContainer: true,

    // hasLoaded: Boolean
    // have template nodes loaded in DOM
    // declarative instance = true
    hasLoaded: false,

    // parentId: String (Optional: passed for declarative instances)
    // parent object id 
    parentId: "",
    
    // displayName: String
    displayName: "",
    
    // hdPath: String
    // header img src
    hdPath: "", 
    
    // isOpen: Boolean
    // is accordion sub nav open
    isOpen: false,

    // activeItemId: String
    // currently active/open child PanelNavs associated w/ this Accordion
    activeSubItemId: "",
   
    // open/close animation properties
    durationOpen: 0.4,     
    durationClose: 0.3,      
    durationFade: 0.3, 

    initialize: function($super, properties) { 
        this.setProperties(properties); 
        //console.log("Accordion.initialize "+this.id);
        
        /**MK: still used?
        if (this.templateType && this.templateType !== "") {  
            this.templatePath = "/js/brand/globalnav/templates/" + this.templateType + ".html";
          }**/
          
        $super();
    }, 
    postCreate: function() {  
        //console.log("Accordion.postCreate "+this.id);
        
        // set header img states
        var hdNode = $(this.id + "_hd"); 
        this.hdImg = new brand.img(hdNode, ["off","on","sel"]);
    },
 
    onClick: function() {    
        //console.log("Accordion.onClick: "+this.id);
        
        // tell parent of event
        if (this.parent) {
            this.parent.onChildClick(this.id);
        }   
 
        // toggle state
        if (this.isOpen) {
            this.close();
        } else {
            this.open();
        }   
       
    },

    open: function() {
         //console.log("Accordion.open "+this.id);  
         if (this.hdImg) this.hdImg.changeSrc("sel"); // switch img 
 
         this._showSubNav();
         this._setActive(true);  
         
         generic.events.fire({event:"accordion:open", msg:{ type: "accordion", id: this.id, parentId: this.parentId, displayName: this.displayName }});        
    },
    
    close: function(/* String */defaultId) {
        // summary:
        //      Closes the active accordion, unless it is associated with default item
        //      Close subitems (panelsubnavs) 
          
        //console.log("Accordion.close "+this.id + " " + defaultId); 
         
        // handle subitems
        if (this.activeSubItemId) {
            var activeSubItem = $(this.activeSubItemId).widget;  
            activeSubItem.close(); 
        } 
        // handle this accordion
        if (this.id !== defaultId) {
            if (this.hdImg) {
                this.hdImg.changeSrc("off"); // switch img
            }
            this._hideSubNav();
            this._setActive(false);
            generic.events.fire({event:"panelnav:hide", msg: { type: "accordion", id: this.id, parentId: this.parentId }}); 
        } 
    },
    
    _setActive: function(/* Boolean */state) {     
        this.isOpen = state;
         
        if (this.parent) {
            if (state == true) {
                this.parent.activeItemId = this.id;
            } else if (this.parent.activeItemId === this.id) {
               this.parent.activeItemId = "";
            }
        } 
    },
    
    _showSubNav: function() {
        var node = this.containerNode; 
        //console.log("Accordion._showSubNav "+ node.id); 
        node.setOpacity(0);
        node.style.overflow = "hidden";
        if (node.style.visibility == "hidden" || node.style.display == "none") { 
            node.style.height = "1px";
            node.style.display = "block";
            node.style.visibility = "";        
        } 
           
        var duration = this.durationOpen;
        var h = node.scrollHeight; 
        
        var expandAccordion  = function() { 
            new Effect.Morph( node, {
                duration    : duration, 
                style       : { height  : h + "px"},   
                afterFinish : fadeUp
            });
        };
        duration = this.durationFade;
        var fadeUp  = function() {  
            new Effect.Opacity ( node, {
                duration    : duration, 
                transition  : Effect.Transitions.linear, 
                from        : 0,
                to          : 1 
            });
        };   
        expandAccordion();   
    },
    
    _hideSubNav: function() {
        var node = this.containerNode;
        //console.log("Accordion._hideSubNav "+ node.id);  
        
       var duration = this.durationClose;
       var contractAccordion = function() {  
            new Effect.Morph(node, {
                duration    : duration, 
                style       : { height  : "1px" },   
                afterFinish : function() {
                    node.hide();
                    node.style.overflow = "hidden";
                }
            });
        };
        duration = this.durationFade;
        var fadeDown  = function() {  
            new Effect.Opacity ( node, {
                duration    : duration,  
                transition  : Effect.Transitions.linear, 
                from        : 1,
                to          : 0 
            });
         };   
         fadeDown();
         contractAccordion();      
    }, 
    
    _onMouseOver: function(e) {
        if (this.hdImg.changeSrc && !this.isOpen) {
            this.hdImg.changeSrc("on");
        }
    },
    
    _onMouseOut: function(e) { 
        if (this.hdImg.changeSrc && !this.isOpen) { 
            this.hdImg.changeSrc("off"); 
        }
    }
}); 
brand.globalnav.Detail = Class.create(Widget, 
{
    // summary:
    //      Most detailed level in heirarchy: includes description, thumbnail & url      
    //templatePath: "/js/brand/globalnav/templates/Detail.html",
    templatePath: "jsTemplates.globalnav.Detail",
    //templateType: "detail",    

    //simpleDetailPath: "/js/brand/globalnav/templates/SimpleDetail.html",
    simpleDetailPath: "jsTemplates.globalnav.SimpleDetail",
    
    // parentId: String
    // PanelSubnav/ProductCategorySubnav widget parent
    parentId: null,
    
    // displayName: String
    displayName: "",
    
    // hdPath: String
    // header img src
    hdPath: "",

    // hdStates: Array
    // possible suffixes for header img src states   
    hdStates: ["off","on","sel"],
    
    // thumbPath: String
    // thumbnail img src
    thumbPath: "",

    // thumbRolloverPath: String (optional)
    // thumbnail rollover img src
    thumbRolloverPath: null,
    
    // description: String
    description: "",
    
    // url: String
    // template href value
    url: "",
    
    // isdefault: Boolean
    // flagged true if item is default item (i.e. product item corresponding w/ current spp)
    isdefault: false,
    
    // isInDefaultCategory: Boolean
    // flagged true if item is within default category
    isInDefaultCategory: false,

    // baseClass: String (optional)
    // supplemental css class, ex: panelnav_link_category   
    baseClass: "", 

    offImg: "off",
    
    timer: null,    
    timerDuration: 3,
    
    initialize: function($super, properties) { 
        this.setProperties(properties);
        //console.log("Detail.init "+this.id); 
        
        if (this.template) {
            var t = this.template;
            if (t.detail) {
                var type = t.detail.type;
                if (type) { this.templatePath = type; }
                var baseClass = t.detail.baseClass;
                if (baseClass) { this.baseClass = baseClass; }
                var hstates = t.detail.headerStates;
                if (hstates) { this.hdStates = hstates; }
            }
        }
          
        $super();
    },
    
    postCreate: function() { 
        //console.log("Detail.postCreate "+ this.id + " / " + $(this.id)); 
           
        // header image 
        var hdNode = $(this.id + "_hd");
        if (hdNode) this.hdImg = new brand.img(hdNode, this.hdStates);

        if (this.isInDefaultCategory) {         
            if (this.isdefault) { 
                this.setDefaultState();
            } else {
                this.offImg = "sel"; // exception for "dimmed" style: "sel" state used for off state of hdImg
                this.setDefaultCategoryState();
            }
        }
  
        // thumbnail rollover
        var thumbImg = $(this.id + "_thumb");
        if (this.thumbRolloverPath && thumbImg) {
            this.thumbImg = thumbImg;
            var preload = new Image();
            preload.src = this.thumbRolloverPath; 
        }
    },
        
    _onMouseOver: function(e) {
        if (this.isdefault) return;
        var event = e || window.event;
        if (this.timer) clearTimeout(this.timer);
        if (this.hdImg) this.hdImg.changeSrc("on");
        if (this.thumbImg) this.thumbImg.src = this.thumbRolloverPath;         
        this.domNode.addClassName("panelnav_link_on");
        Event.stop(event);
    },
    
    _onMouseOut: function(e) {
        if (this.isdefault) return;
        var event = e || window.event;
        Event.stop(event);
        var self = this;
        var out = function() {
            if (self.hdImg) self.hdImg.changeSrc(self.offImg);
            if (self.thumbImg) self.thumbImg.src = self.thumbPath;        
            self.domNode.removeClassName("panelnav_link_on");
        }
        
        // IE workaround for item bg flickering. cancels bubbling unless mouse is totally out of item
        if (generic.env.isIE) {
            this.timer = setTimeout(out, this.timerDuration);
        } else {
            out();
        }
    },
    
    _onClick: function(e) {
        //console.log("Detail._onClick "+this.domParent );  
        // workaround for ie: use location prop as back-up for href
        if (this.url && generic.env.isIE) {
            location.href = this.url;
        }
    },
    
    setDefaultState: function() {
        //console.log("Detail.setDefaultState "+this.id+"/"+$(this.id));
        if (this.hdImg) {
            // most sites use on state for rollover & spp "default" state vs. others which 
            // use distinct xxx_active.gif img
            var activeImg = "on";
            var hdStates = this.hdStates;
            hdStates.any(function(state) { 
                if (state === "active") {
                    activeImg = state;
                    return true;
                } 
            });
            this.hdImg.changeSrc(activeImg);
        }
        
        this.domNode.addClassName("panelnav_default");
        
        // if url matches current page, remove link
        var loc = window.location.pathname;
        if (loc.indexOf(this.url) > -1) { 
            $(this.id).removeAttribute("href");
            this._onClick = function() { };
            $(this.id).addClassName("unclickable");
        } 
    },
    
    setDefaultCategoryState: function() {
        // displays "dimmed" state for detail items in same category as default item
        if (this.hdImg) this.hdImg.changeSrc(this.offImg);
    },
    
    reset: function() {
       this.destroy();
    }
});
 
brand.globalnav.ProductCategoryDetail = Class.create( brand.globalnav.Detail,
{
    // summary:
    //      Category detail-level of heirarchy:
    //      Includes details of category item, but is not the final level in nav heirarchy.
    //      onClick event triggers display of category list (Accordion).
    //
    //      Receives id args for widget parent ProductSubNav (parentId) & DOM parent (containerId)
    //      Refers to parent class method(s) for talking back up the heirarchy chain
    //      Uses container for placement in DOM only
    //
    //      Receives id arg for cousin Accordion to pass to parent.openAccorion
    
    //templatePath: "/js/brand/globalnav/templates/ProductCategoryDetail.html",
    templatePath: "jsTemplates.globalnav.ProductCategoryDetail",
    
    // containerNode: DOM node
    // container for CategoryDetail item
    containerNode: null,
    
    // accordionId: String
    // Accordion widget id
    accordionId: null,
    
    initialize: function($super, properties) { 
        //console.log("Detail.ProductCategoryDetail.init");
        if (this.containerNode) {
            this.domParent = this.containerNode; 
        }  
        $super(properties);  
    }, 
    
    _onClick: function(e) {
        //console.log("ProductCategoryDetail._onClick: "+this.domParent ); 
        // hide category detail
        this.containerNode.style.display = "none";
        // show accordion
        $(this.parentId).widget.getAccordion(this.accordionId);
    }
    
});

brand.globalnav.CollectionCategoryDetail = Class.create( brand.globalnav.ProductCategoryDetail,  
{
    
    //templatePath: "/js/brand/globalnav/templates/CollectionCategoryDetail.html", 
    templatePath: "jsTemplates.globalnav.CollectionCategoryDetail",
        
    isContainer: true,

    startup: function($super, arguments) {
        this.containerNode = $(this.parentId);
        this.accordionId = this.id+"_accordion";
        $super(arguments);
        this.parent = $(this.parentId).widget;
    },
    
    _onClick: function(e) {
        //console.log("CollectionCategoryDetail._onClick: "+this.categoryDetailNode.id); 
        // hide category detail
        this.categoryDetailNode.style.display = "none";
        // show accordion
        this.parent.getAccordion(this.accordionId);
    },
    
    onChildClick: function(/* String */sectionId) {
        //this.parent.onChildClick();
    }
    
});

brand.globalnav.SearchProductDetail = Class.create(brand.globalnav.Detail, 
{ 
    //templatePath: "/js/brand/globalnav/templates/SearchProductDetail.html", 
    templatePath: "jsTemplates.globalnav.SearchProductDetail",
        
    hex: "",
    actionImg: null,
    
    initialize: function($super, arguments) {
        //console.log("brand.globalnav.SearchProductDetail init"); 
        $super(arguments); 
    },
    startup: function($super, arguments) {
        //console.log("brand.globalnav.SearchProductDetail.startup"); 
        var actionImgNode = $(this.id + "_actionimg");         
        if (actionImgNode) this.actionImg = new brand.img(actionImgNode, ["off","on"]);
    },
    
    _onMouseOver: function($super, e) {
        $super(e);
        if (this.actionImg) this.actionImg.changeSrc("on");
    },
    
    _onMouseOut: function($super, e) {
        $super(e);
        if (this.actionImg) this.actionImg.changeSrc("off");
    }
 
});


brand.globalnav.SearchQuickBuyDetail = Class.create(brand.globalnav.Detail, 
{
    
    //templatePath: "/js/brand/globalnav/templates/SearchQuickBuyDetail.html", 
    templatePath: "jsTemplates.globalnav.SearchQuickBuyDetail",
        
    isContainer: true,
    product: null,
    hex: "",
    skupath: "",
    cartConfirmMsg: null,
    shadedResult: false,
    shaded: false,

    initialize: function($super, arguments) {
        //console.log("brand.globalnav.SearchQuickBuyDetail.init");
        $super(arguments);
    },
    postCreate: function($super, arguments) {
        $super(arguments);
        this.shadedResult = (this.product.shade_result ? true : false),
        this.skupath = this.product.sku.path;
        this.shaded = (this.product.shaded ? true : false);  
    },
    
    startup: function($super, arguments) {
        //console.log("brand.globalnav.SearchQuickBuyDetail.startup "+this.shadenameNode + "/" + this.descriptionNode);
 
        if ( this.shadedResult ) {
            this.shadenameNode.removeClassName("hidden");
        } else {
            this.descriptionNode.removeClassName("hidden");
        }
        
        this._initCartAction();
    },
    
    _initCartAction: function() {
        var shoppable = (this.product.sku.shoppable === "1" ? true : false);
        var self = this;
                
        if ( shoppable ) {
            //console.log("brand.globalnav.SearchQuickBuyDetail._initCartAction shoppable");
            this.cartConfirmMsg = new brand.product.cartConfirm({
                id: "search_cart_confirm-" + this.skupath,
                is_shaded: this.shaded,
                prodName: this.displayName,
                sku: this.product.sku,
                nodeToReplace: this.cartConfirmNode
            });
            
            //console.log("brand.globalnav.SearchQuickBuyDetail._initCartAction shoppable");
            // add to cart button 
            var btnNode = $(this.id + "_btn_add");
            btnNode.value = this.skupath;
            var button = brand.product.addButton({
                addButtonNode: btnNode,
                callback: function(response) {
                    self.cartConfirmMsg.show({ response: response });
                }
            });
        
        } else {
            //console.log("brand.globalnav.SearchQuickBuyDetail._initCartAction !shoppable");
            var btn = $(this.id + "_btn_add");
            if (btn) {
                btn.style.display = "none";
            }
            this.inventoryStatusNode.innerHTML = this.product.sku.inventory_status_message;
            this.inventoryStatusNode.style.display = "block";
        }
    },
    
    reset: function() {
        if (this.cartConfirmMsg) {
            this.cartConfirmMsg.destroy();
        }
        this.destroy();
    }
     
});

 
brand.globalnav.DiscontinuedProductDetail = Class.create( 
    brand.globalnav.Detail,
{
    
    //templatePath: "/js/brand/globalnav/templates/DiscontinuedProductDetail.html", 
    templatePath: "jsTemplates.globalnav.DiscontinuedProductDetail",
        
    sku: null,
    hex: "",
    shadename: "",
    shadedResult: false,

    initialize: function($super, properties) { 
        $super(properties); //calls Detail.postMixInProperties  
        
        if ( this.shadedResult && this.sku ) {
            this.hex = this.sku.HEX_VALUES;
            this.shadename = this.sku.SHADENAME;
            this.url += "&SKU_ID=" + this.sku.SKU_ID;
        }    
    },   
    
    startup: function($super, arguments) {
        //$super(arguments);
        
        /*if ( this.shadedResult ) {
            this.shadenameNode.removeClassName("hidden");
        } */      
    } 
});// summary:
//      PanelSubNav base class & extensions:
//      PanelSubNav, ProductSubNav

brand.globalnav.PanelSubNav = Class.create(Widget, 
{
    // summary:
    //      Contains subnav content for each PanelNav
    //      DOM child of Panel

    //templatePath: "/js/brand/globalnav/templates/PanelSubNav.html",   
    templatePath: "jsTemplates.globalnav.PanelSubNav",  
            
    isContainer: true,
    
    // parentId: String
    // id of PanelNav that displayed me
    parentId: "",

    // activeItemId: String
    // currently active/open child widget id
    activeItemId: "",

    // dataId: String
    // id as defined in json (ex: supercat id)
    dataId: "",

    // content: Object
    // contains parameters for requesting items from server, ex: content.url
    content: null,
    
    // isDefaultPanel: Boolean
    // true if panel is the open/page panel
    isDefaultPanel: false,

    // hasLoaded: Boolean
    // flag set to true when content has been rendered as html (true if declarative or after json request & render)
    hasLoaded: false,
    
    // cache: Boolean
    // cache after first request for content
    cache: true,
    
    initialize: function($super, properties) {
        this.setProperties(properties);
        //console.log("PanelSubNav.initialize: "+this.id + " / "+this.parentId + " " +this.isDefaultPanel+" properties = ",properties);
        
        // check for properties stored in this.content config settings
        var content = this.content;
        if (content) {
            if (content.reinsertNode) this.reinsertNode = content.reinsertNode;
            if (content.cache) this.cache = content.cache;
            if (content.hasLoaded) this.hasLoaded = content.hasLoaded;
        }
        
        if (this.isDefaultPanel) {
            this.domParent =  "panel_open"; 
        } else { 
            this.domParent = $($(this.parentId).widget.panelId).widget.containerNode;  
        }
        //console.log("PanelSubNav.initialize add subnav "+this.id + " / parent "+ this.parentId);

        $super();
    },  
    
    postCreate: function() {
        //console.log("PanelSubNav.postCreate "+this.id+ " / "+$(this.id)); 
        
        // method won't exist for the default panel on page, bc that content is in panel_open
        var parent = $(this.parentId);
        if (parent && parent.widget && parent.widget.addSubNav) {
             parent.widget.addSubNav(this);      
        }
       
        // show instances that are loaded, but hidden before init (i.e. my mac)
        if (this.hasLoaded) {
            //console.log("PanelSubNav.postCreate "+this.id+ " / "+$(this.id)+" reinsertNode = "+this.reinsertNode);
        
            this.showProgress(false);
            //psubnav.domNode.removeClassName("invisible"); 
            //$(this.id).removeClassName("invisible");
            this.containerNode.removeClassName("invisible");
        }
        
        if (this.callback) {
            //console.log("PanelSubNav.postCreate "+this.id+ " > callback");
            this.callback(); 
        }
    },

    onChildrenLoaded: function(args) {
        this.showProgress(false);
        this.containerNode.removeClassName("invisible"); // show loaded content
        
        if (args && args.hasLoaded) {
            this.hasLoaded = true;
        }
    },
    
    addSubItem: function(/* DOM node ? String */item,/* DOM node? */container) {
        // summary:
        //      Places child node(s)
        //      If passed a string, create a container node for placement 
        //console.log("PanelSubNav.addSubItem "+container+" / " + typeof container);
               
        if (!container) {
             container = this.containerNode || $(this.id); 
        }
        /**
        if (!container && this.containerNode) {
            container = this.containerNode; 
        } else {
            container = $(this.id); 
        }**/
        
        var node = item;
        if (typeof(item) === "string") {
            node = document.createElement("div");
            node.innerHTML = item;
            this.hasLoaded = true;
        }
        try {
            container.appendChild(node);
        }
        catch (e) { 
            console.log("PanelSubNav.addSubItem e: "+e.description);  
        }
    },
    
    onChildClick: function(/* String */sectionId) {
        // summary:
        //      Called by child widgets
        //      Captures events occuring further down in navigation heirarchy

        if (this.activeItemId && (this.activeItemId !== sectionId)) {
            var activeItem = $(this.activeItemId).widget;
            activeItem.close();
        }   

    },
    
    onParentClick: function() {
    },
    
    showProgress: function(/* Boolean */state) {
        this.progressNode.style.display = (state) ? "block" : "none"; 
    },
    
    setDefaultState: function() {
        $(this.id).addClassName("panelnav_category_default");
    }
    
});


brand.globalnav.ProductSubNav = Class.create( brand.globalnav.PanelSubNav,
{
    // summary:
    //      Contains product catalog subnav content for each PanelNav:
    //      category-level (ProductCategoryDetail) items and category list (Accordion) items.
    //      Detail-level (ProductDetail) items contained within category Accordion

    templateString: null,    
    //templatePath: "/js/brand/globalnav/templates/ProductSubNav.html",  
    templatePath: "jsTemplates.globalnav.ProductSubNav", 
        
    // inAccordionMode: Boolean
    // is subnav showing accordion items (rather than category detail items)
    inAccordionMode: false,

    // activeAccordionId: String
    // currently active/open child Accordion    
    activeAccordionId: "",
    
    initialize: function($super, properties) {
       //console.log("ProductSubNav.init"); 
       $super(properties);  
    },

    addCategoryDetail: function(/* Object */args) {
        // summary:
        //      Creates category detail element
        //      Places ProductCategoryDetail in detailContainerNode

        //console.log("ProductSubNav.addCategoryDetail "+args.id + "/ "+this.id);
        args.parentId = this.id; // this sub nav
        //JSTEST args.parentId =  $(this.id).widget.parent.id;
        args.containerNode = this.detailContainerNode; //JSTEST bc of Detail._onClick, change?
        args.domParent = this.detailContainerNode; // ProductCategoryDetail container 
        args.accordionId = args.id+"_accordion"; // id of corresponding Accordion 
             
        var detail = new brand.globalnav.ProductCategoryDetail(args);
        //detail.startup(); // SS: commented out since brand.globalnav.ProductCategoryDetail had an empty startup method
    },
    
    addCategoryAccordion: function(/* Object */args) {
        // summary:
        //      Creates category list/accordion element
        //      Places Accordion in accordionContainerNode
        //      Returns instance of Accordion
        //console.log("ProductSubNav.addCategoryAccordion "+args.id+"_accordion to "+this.accordionContainerNode.id);
        args.accordionId = args.id+"_accordion";
        
        var accordion = new brand.globalnav.Accordion({
            id: args.accordionId,
            displayName: args.displayName,
            domParent: this.accordionContainerNode,
            hdPath: args.hdPath,
            parentId: this.id
        });
        //JSTEST this.accordionContainerNode.appendChild(accordion.domNode);
        //accordion.startup(); // SS: accordion class no longer has startup method
                                    
        return accordion;
    },
    
    getAccordion: function(/* String */id) {
        var accordion = $(id).widget;
        this.openAccordion(accordion);
    },
    
    openAccordion: function(/* Widget */accordion) {
        //console.log("ProductSubNav.openAccordion "+accordion.id);  
        // show accordion container
        this.accordionContainerNode.style.display = "block"; 
        this.inAccordionMode = true;
        // show accordion nav
        accordion.open();
        this.activeAccordionId = accordion.id;
    },

    setCategoryState: function(args) {
        //console.log("ProductSubNav.setCategoryState "+args.accordion.id);  
        if (args.isDefaultCat && args.useAccordionMode) {
              //console.log("ProductSubNav.setCategoryState "+args.accordion.hdNode);  
       
            this.accordionContainerNode.removeClassName("hidden");
            var accordion = args.accordion;
            
            // show items in category under accordion
            function openAccordion() {
                accordion.open();
            }
            openAccordion.delay(1);
            
            this.detailContainerNode.style.display = "none";
            // apply subnav group style if there is default detail item to show
            if (args.hasItemInDefaultCategory) {
                $(accordion.id).addClassName("panelnav_category_default");
            }            
        }
    },

    setPanelState: function(args) {
        //if category level includes non-category (childless) items      
        if (args.hasMixed) {
            $(this.id).addClassName("panelnav_subnav_mixed");
            this.detailLinksContainerNode.style.display = "block";           
        }
    },
    
    reset: function() {
        // summary:
        //      Resets default state of subnav:
        //      shows ProductCategoryDetail container, hides Accordion container
        //console.log("ProductSubNav.reset"); 
        if (this.inAccordionMode) {
            this.detailContainerNode.style.display = "";
            this.accordionContainerNode.style.display = "";
            $(this.activeAccordionId).widget.close();
            this.inAccordionMode = false;
            this.activeAccordionId = "";
        }
    },
    
    onParentClick: function() {
        this.reset();
    }
    
});

brand.globalnav.SectionDescSubNav = Class.create( brand.globalnav.PanelSubNav,
{
    // summary:
    //      PanelSubNav with additional content attributes.  Contains:
    //      Large Section header & description text
    //      Detail-level (Detail) items

    templateString: null,    
    templatePath: "jsTemplates.globalnav.SectionDescSubNav", 
        
    // hdAlt: String
    hdAlt: "",

    // description: String
    description: "",

    setContent: function(args) {
        // populate additional content nodes
        this.panelDescriptionNode.innerHTML = args.description;
        this.hdNode.setAttribute("src", args.header);
        this.hdNode.setAttribute("alt", args.header_alt);
    },
    
    onChildrenLoaded: function(args) {
        this.showProgress(false);
        this.contentNode.removeClassName("invisible"); // show loaded content
        this.hasLoaded = true;
    }
    
});

brand.globalnav.VideosSubNav = Class.create( brand.globalnav.SectionDescSubNav,
{
    // summary:
    //      PanelSubNav with additional content attributes.  Contains:
    //      Large Section header & description text
    //      Detail-level (Detail) items
    //      "Previous" button

    templateString: null,
    //templatePath: "/js/brand/globalnav/templates/VideosSubNav.html",
    templatePath: "jsTemplates.globalnav.VideosSubNav",
    itemCount: 0,
    featuredMax: null,
    domInsertionMethodName: "addSubItem", // tell GlobalNav class to use specific method for DOM Insertion to override generic.widget class default DOM insertion (insertMixIn)

    initialize: function($super, properties) {
        $super(properties);
        this.featuredMax = this.content.featuredMax;
    },

    postCreate: function($super) {
        $super();
        // set up event handler for "previous" button
        var previousBtn = $("psubnav_artistry_in_action_btnprevious");
        var btnContainer = this.containerNode.select("div.psubnav_artistry_in_action_btn_container")[0];
        var self = this;
        if (btnContainer && previousBtn) {
            previousBtn.observe("click", function (clickEvent) {
                clickEvent.preventDefault();
                btnContainer.hide();
                self.previousContainerNode.style.display = "block";
                // this isn't an accordion object, but the expansion functionality is the same, so we still want to broadcast the event for brand.view.heightHandler
                if (!self.isDefaultPanel) {
                    generic.events.fire({event:"accordion:open", msg:{ type: "accordion", id: self.id, parentId: self.parentId }}); 
                }
            });
        }
        
    },
   
    addSubItem: function(item) {
        // summary:
        //      Places child node(s)
        this.itemCount++;
        //console.log("this.itemCount = "+this.itemCount+" this.featuredMax = "+this.featuredMax);
        if (this.itemCount <= this.featuredMax) {
            container = this.featuredContainerNode; 
        } else {
            container = this.previousContainerNode;
        }
        
        var node = item;
        if (typeof(item) === "string") {
            node = document.createElement("div");
            node.innerHTML = item;
            this.hasLoaded = true;
        }
        try {
            container.appendChild(node);
        }
        catch (e) { 
            console.log("ArtistryInActionSubNav.addSubItem e: ",e);  
        }
    }
    
});brand.globalnav.Header = Class.create(Widget, 
{
    // summary:
    //      Renders header image for item that doesn't open a Panel or Accordion  
    templateLi: "jsTemplates.globalnav.headerLi",
    templateDiv: "jsTemplates.globalnav.headerDiv",

    // displayName: String
    displayName: "",
    
    // hdPath: String
    // header img src
    hdPath: "",

    // description: String
    description: "",
    
    // url: String (optional)
    // template href value
    url: "",

    // isdefault: Boolean
    // Item is (section, page, etc...) associated with default page state display
    isdefault: false,
    
    // hasLoaded: Boolean
    // have template nodes loaded in DOM
    // declarative instance = true
    hasLoaded: false,
    
    // parentId: String (optional)
    // parent object id
    // used to check status of other menus in a set (ex: if this is default item & clicking on it should close open menus to bring focus back to default)
    parentId: "",

    initialize: function($super, args) {
        this.setProperties(args);
        // set template group based on parent node element type
        this.templatePath = this.templateLi;
        var preNode = $(args.id);
        if (preNode) {
            var parentNode = preNode.parentNode;
        } else {
            var parentNode = $(args.parentId);
            if (!parentNode) {
                console.log("brand.globalnav.Header: Node UNDEFINED for args.parentId: "+args.parentId);
                return false;
            }
            var parent = parentNode.widget; 
            if (parent) {
                var parentNode = (parent.containerNode ? parent.containerNode : parent.domNode);
            }
        }  
        
        // eg Gift Card
        if (parentNode && parentNode.nodeName === "DIV") { 
            this.templatePath = this.templateDiv;
        }

        this.removeLink = (args.isdefault || !args.url);
        
        //console.log("brand.globalnav.Header "+args.id);
        $super();
        this.startup();
    },

    startup: function() {
        //console.log("STARTUP id = "+this.displayName+" isdefault = "+this.isdefault);
        if (this.removeLink) {
            var linkNode = $$("#"+this.id+" a")[0];
            if (linkNode) {
                linkNode.removeAttribute("href");
                linkNode.addClassName("unclickable"); 
                this.containerNode.removeClassName("clickable");
            }
        }    

        // if image is *not* already declared in page
        //console.log("Header: this.hasLoaded = "+this.hasLoaded);
        if (!this.hasLoaded || !this.isdefault) {
            var hdNode = this.hdNode;
            this.hdImg = new brand.img(hdNode, ["off","on","sel"]);
        }
        
        if (this.isdefault) {
           this.setDefaultState();
        } else {
            // set up mouseover events
            this.domNode.observe("mouseover", this._onMouseOver.bind(this));
            this.domNode.observe("mouseout", this._onMouseOut.bind(this));
        }
    },
     
    _showDefault: function(accordionId, parent) {
        //console.log("globalnav.Header._showDefault "+accordionId + " / " + parent); 
        // tell parent set to bring focus back to default
        if (parent.onChildClick && (parent.activeItemId !== "")) {
            parent.onChildClick(accordionId, true);
        }
    },
    
    _onMouseOver: function(e) {
        if (this.hdImg) {
            this.hdImg.changeSrc("on");
        }
    },
    
    _onMouseOut: function(e) {
        if (this.hdImg) {
            this.hdImg.changeSrc("off");
        }
    },
    
    setDefaultState: function() {
        // summary:
        //
        //      Handles state of header image in left nav associated with default/open panel

        //  set "default" state (on/sel)
        if (this.hdImg) this.hdImg.changeSrc("sel");
        
        var parentId = this.parentId;        
        if (parentId.indexOf("psubnav") != -1) { return; } // ignore header instances inside panelsubnav 
                
        var parent = $(parentId).widget;
        var accordionId = "";
        //console.log("globalnav.Header.setDefaultState: "+this.id + "/ parentId = "+parentId+" parent = "+parent); 
        // if parent is accordion in left nav, globalnav_container will be 1 level-up
        if (parentId !== "globalnav_container") {
            accordionId = parentId;
            parentId = parent.parentId;
            parent = $(parentId).widget;
        }
        if (parentId === "globalnav_container") { 
            //console.log("globalnav.Header.setDefaultState: "+this.id + "/" + accordionId +" / "+ parent.id); 
            var self = this;
            var onclick = function() {
                self._showDefault(accordionId, parent);
            }
             
            this.domNode.observe("click", onclick );   
        }
    }
    
});// summary:
//      Classes for elements needed to construct a panel:
//      Panel, PanelNavSet, PanelNav

  
brand.globalnav.PanelNavSet = Class.create(Widget,  
{
    // summary:
    //      Parent controller for multiple PanelNavs & PanelSubNavs associated with
    //      a single Panel (1 to many relationship)
    //      (Ex: Shop Products section only has 1 panel for all its subsections)
        
    isContainer: true,

    // parentId: String
    // parent object id 
    parentId: null,
    
    // isPanelSet: Boolean
    // referenced by PanelNav children to indicate that PanelNav's parent is a PanelNavSet (rather than globalnav container)
    isPanelSet: true,

    // panelId: String
    // Panel to slide in/out    
    panelId: "",
    
    // activeItemId: String
    // currently active/open NavItem
    activeItemId: "",
    
    initialize: function($super, properties) {
        //console.log("PanelNavSet.initialize "+properties.id);   
        $super(properties);
    },

    postCreate: function() {
        //console.log("brand.globalnav.PanelNavSet.postCreate " + this.id);  
        this.panelId = this._addPanel();
    },
    
    setActiveItem: function(/* String */itemId) {
        //console.log("PanelNavSet.setActiveItem: setting active to = "+itemId + " / notify parent "+this.parentId);
        this.activeItemId = itemId;
        this.parent.activeSubItemId = itemId; // tell Accordion
    },
    
    onChildClick: function(/* String */itemId) {
        //console.log("PanelNavSet.onChildClick triggered from item: "+itemId+" to parent: "+this.id+" activeItem id = "+this.activeItemId);
        if (this.activeItemId && (this.activeItemId !== itemId)) {
            this.hideItem(this.activeItemId); 
        }
    },
    
    _addPanel: function() {
        // summary
        //      Add 1 Panel element that contains multiple PanelNavs in this set
        //
        //console.log("PanelNavSet._addPanel "+this.id);
        var panel = new site.layout.Panel({
            id: this.id+"_panel",
            parentId: this.id 
        });
        return panel.id 
    },
    
    // fade in & toggle methods: only used w/ a PanelNavSet (iow when a PanelNav has siblings)
    fadeInSubNav: function(id) {
       var node = $(id); 
        
        // TODO: if panel is already open, we have to start w/ fade in, but if the panel isn't open, we can just do: s.display = ""; before the panel slides out 
        node.hide(); // hide it before opacity is set
        node.setOpacity(0); 
        node.show(); // ok to display now
        
        new Effect.Opacity (node, {
            duration    : 0.33, 
            transition  : Effect.Transitions.linear, 
            from        : 0,
            to          : 1 
        });

    },
    
    hideItem: function(/* String */itemId) {
        //console.log("PanelNavSet.hideItem "+itemId);
        var item = $(itemId).widget;
        if (item.hdImg) item.hdImg.changeSrc("off");
        this.toggleSubNav(item.subId, 0);
        item._setActive(false);
    },
    
    toggleSubNav: function(id, state) { 
        //console.log("PanelNavSet.toggleSubNav "+id+" / " + state);
        $(id).style.display = (state == 1) ? "" : "none"; 
    }
});


brand.globalnav.panelManager = Class.create (Widget, 
{
    // summary:
    //      Handles events from trigger to effect the panel associated with this item

    // hasPanelSiblings: Boolean
    // is this PanelNav part of a PanelNavSet   
    hasPanelSiblings: false,
    
    // panelId: String
    // Panel to slide in/out
    panelId: "",

    // parent: Object
    // parent widget 
    parent: null,
    
    // subId: String
    // id of associated PanelSubNav
    // fades in/out only if it is part of a PanelNavSet
    subId: "",
    
    // sectionId: String
    // brand.globalnav.config section id
    sectionId: "",

    // item: Object (optional)
    // brand.globalnav.config item-level
    item: null,
    
    initialize: function($super, args) {
        //console.log("panelManager.init "+args.id + "/"+args.parentId);
        this.id = args.id;
        this.item = args.item;
        try {
            this.parent = $(args.parentId).widget;
            this.hasPanelSiblings = (!!this.parent.isPanelSet);  //false? 
        } catch(e) {
            console.log("panelManager.init E "+args.id + "/"+args.parentId+" doesn't exist as a dom obj");
        }
        $super(args); 
    },

    startup: function() {
        // used only for panelManager (PanelNav as extension of panelManager is stored as dijit instead)
        this.panelId = this._addPanel(this.parent); 
        //this.parent.addChild(this); //search and other children of Global Set without panels
    },
    
    addSubNav: function(/* Widget */subNavItem) {
        // summary
        //      Captures PanelSubNav item as a sub item (subId) associated w/ this PanelNav
        //      Adds PanelSubNav item as DOM & widget child of the Panel for this PanelNav
        //      Hides PanelSubNav's with siblings (part of a PanelNavSet)
        //console.log("PanelMgr.addSubNav "+subNavItem.id + " / "+this.panelId + " / "+$(subNavItem.id));
        this.subId = subNavItem.id;
        //$(this.panelId).widget.addChild(subNavItem);  
        
        if (this.hasPanelSiblings && $(this.subId) ) {
            $(this.subId).style.display = "none"; 
        }   
    },
    
    _addPanel: function(/* Widget */parent) {
        // summary
        //      If PanelNav instance is part of a set, get its Panel instance already created by PanelNavSet
        //      Else PanelNav instance gets a Panel of its own
       
        //console.log("panelManager._addPanel "+this.id + "_panel / " + this.id + " / " + this.parentId);
        var id; 
        if (this.hasPanelSiblings) {
            id = parent.panelId;
        } else {
            panel = new site.layout.Panel({
                id: this.id + "_panel",
                parentId: this.id, 
                domParent: "panel_container"
            });
            id = panel.id;
        }
        return id;
    },
    
    _setActive: function(/* Boolean */state) {   
        //console.log("PanelManager._setActive");
        this.isActive = state;
        if (state == true) {
            this.parent.setActiveItem(this.id);
        } else if (this.parent.activeItemId === this.id) {
            this.parent.setActiveItem("");
        }
    },
    
    _onClick: function(e) {
        // summary:
        //      Click handler for PanelNav button (in template)

        //console.log("panelManager._onClick() this.id: "+this.id+ " / this.subId: "+this.subId);
   
        if (!this.isActive && this.subId) {
            var psubnav = $(this.subId).widget;
            if (!psubnav.hasLoaded || !psubnav.cache) {
                // wait for the panel to open before starting load (avoids animation stutters)
                // Panel.durationOpen = 400
                var si = this.sectionId; 
                var d = this.item;
                var di = this.itemId;
                var g = function() { 
                   params = { psubnav: psubnav, sectionId: si, item: d, itemId: di };
                   generic.events.fire({event:"panelnav:click", msg:params}); 
                }               
                setTimeout(g, 400);
            }
        }
        
        this.onTrigger(false);
    },

    onTrigger: function(stayOpen) {
        // summary:
        //      Handler for any "trigger" event (i.e. button click or search submission)
        //      stayOpen keeps panel from closing if it is already active (ex: search)
        
        //console.log("PanelMgr.onTrigger "+this.id + " / parent "+this.parentId + " / subnav " + this.subId);
       
        $(this.parentId).widget.onChildClick(this.id); // parent is either GlobalSet or PanelNavSet
         
        var psubnav = $(this.subId).widget; // psubnav (base class: PanelSubNav)
        // toggle state
        if (this.isActive) {
            if  (!stayOpen) {
                this.hideItem();
            }
        } else {
            psubnav.onParentClick();
            this.showPanel();
            if (!psubnav.hasLoaded || !psubnav.cache) {             
                if (!psubnav.hasLoaded && psubnav.progressNode) {
                    psubnav.progressNode.style.display = "block"; // show progress   
                }               
            }
        }
    },
    
    close: function() {
        // summary:
        //      Called from parent Accordion
        //      note: "close" used as generic name (matches Accordion method name)
        
        this.hideItem();
    },
    
    showPanel: function() {
        // summary:
        //      For single PanelNav: Panel slides out
        //      For PanelNavs w/ siblings: PanelSubNav fades in
        
        //console.log("brand.globalnav.PanelManager.showPanel"); 
        var panel = $(this.panelId).widget;
        
        if (this.hasPanelSiblings) {
            var parentSet = this.parent;            
            if (panel.isOpen) {
                //console.log("PanelManager.showPanel parentSet.fadeInSubNav")
                parentSet.fadeInSubNav(this.subId);
            } else {
                //console.log("PanelManager.showPanel parentSet.toggleSubNav") 
                parentSet.toggleSubNav(this.subId, 1);
                panel.open();
            }
        } else {
            panel.open();
        }
        
        this._setActive(true); 
        generic.events.fire({event:"panelnav:show", msg:{ type:"panel", id: this.panelId, itemId: this.itemId, subId: this.subId, sectionId: this.sectionId, displayName: this.displayName, parentId: this.parentId }}); 
    },
    
    hideItem: function() {
        // summary:
        //      For single PanelNav: Panel slides in
        //      For PanelNavs w/ siblings: PanelSubNav fades in
        //console.log("PanelManager.hideItem"); 
        var panel = $(this.panelId).widget;
        panel.close();
        if (this.hasPanelSiblings) {
            this.parent.toggleSubNav(this.subId, 0);
        }
        this._setActive(false);
        
        generic.events.fire({event:"panelnav:hide", msg: { type: "panel", id: this.panelId, itemId: this.itemId, subId: this.subId }} );
    }   
});


brand.globalnav.PanelNav = Class.create( brand.globalnav.panelManager, 
{
    // summary:
    //      Creates the trigger header in the left nav
    //      Handles events from trigger to effect the panel associated with this item
    //      Can be an child of a PanelNavSet (hasPanelSiblings = true),
    //      or a child of a single Panel if it is not part of a set (1 to 1 relationship)
    //      (Ex: My Mac section has 1 Panel with 1 PanelNav & 1 PanelSubNav)

    //templatePath: "/js/brand/globalnav/templates/PanelNav.html",
    templatePath: "jsTemplates.globalnav.PanelNav",
   
    // hasLoaded: Boolean
    // have template nodes loaded in DOM
    // declarative instance = true
    hasLoaded: false,
    
    // parentId: String
    // parent object id 
    parentId: "",
    
    // displayName: String
    displayName: "",
    
    // hdPath: String
    // header img src
    hdPath: "",
    
    // img: brand.img object
    hdImg: {},
    
    // isActive: Boolean
    // is this PanelNav active/visible    
    isActive: false,

    // itemId: String (optional)
    // brand.globalnav.config item-level id  
    itemId: "",
    
    initialize: function($super, properties) { 
        //console.log("PanelNav.init "+properties.id + "/"+properties.parentId + "/"+properties.domParent); 
        $super(properties);
    },
    postCreate: function() {
        //console.log("PanelNav.postCreate "+this.id+" to parent "+this.parentId);    
    
        if (!this.parent) { 
            //console.log("PARENT NOT FOUND w/ dijit.byId");
            //this.parent = this.getParent();
        }

        if (this.itemId === "") {
            try {
                this.itemId = this.item.id;
            }
            catch (err) { }
        }
        
        // declarative instance
        if (this.hasLoaded) {     
            //this.parent.addChild(this.id);  
            this.startup();
        } 
    },
    
    startup: function() {
        // summary
        //      Called from postCreate if declarative instance
        //      Called explictly if programmatic instance (Accordion has to wait for header to be placed in DOM)
        
        //console.log("PanelNav.startup "+this.parent.id);
        this.panelId = this._addPanel(this.parent); //moved from PanelMgr's initialize
        
        var hdNode = $(this.id + "_hd");
        this.hdImg = new brand.img(hdNode, ["off","on","sel"]);
    },

    showPanel: function($super, args) {
        // summary:
        //      Extends panelManager.showPanel
        
        //console.log("PanelNav.showPanel "+this.id);
        $super(args);
        
        if (this.hasPanelSiblings) {
            if (this.hdImg) this.hdImg.changeSrc("sel"); // gnav category img
        } else {
            if (this.hdImg) this.hdImg.changeSrc("on"); // gnav section img
        }
    },
    
    hideItem: function($super, args) {
        // summary:
        //      Extends panelManager.hideItem
        
        $super(args);
        
        if (this.hdImg) this.hdImg.changeSrc("off");
    },
    
    _onMouseOver: function(e) { 
        if (this.hdImg && !this.isActive) this.hdImg.changeSrc("on");
    },
    
    _onMouseOut: function(e) {
        if (this.hdImg && !this.isActive) this.hdImg.changeSrc("off");
    }
});

site.layout = {};//JSTEST temp
site.layout.Panel = Class.create(Widget, 
{
    // summary:
    //      Creates visual representation of sliding panel element with close button
    //      Can contain a single PanelSubNav or multiple navs as part of a PanelNavSet 
    //templatePath: "/js/brand/globalnav/templates/Panel.html",        
    templatePath: "jsTemplates.globalnav.Panel",  
    
    // isOpen: Boolean
    // Panel open/closed state
    isOpen: false,
    
    // parentId: String
    // pointer to PanelNav or PanelSet that spawned me
    parentId: "",
    domParent: "panel_container",
    
    // panel animation properties 
    closedpx: -96,
    openpx: 192,
    durationOpen: 0.4,
    durationClose: 0.3,
    
    initialize: function($super, properties) {
        this.setProperties(properties);  
        //console.log("site.layout.Panel.initialize "+this.id+ " / "+this.parentId + " / "+this.domParent); 
        $super();   
    },
    postCreate: function() {
        // place panel in panel container 
        //console.log("site.layout.Panel.postCreate"); 
        //$("panel_container").innerHTML += this.domNode;
    },
    
    _onClickClose: function() {
    // summary:
    //      Handler for close button on panel
    //      If this is a set, get active PanelNav via PanelNavSet
    //      Else, talk to PanelNav directly
        //console.log("site.layout.Panel._onClickClose()"); 
        var parent = $(this.parentId).widget;
        
        // otherwise, look for parent instance stored by GlobalSet
        if (!parent) {
            try {
                var gset = $(globalNavSetId);
                parent = gset.getChild(this.parentId);
            }
            catch(err){};
        }
            
        if (parent.isPanelSet) { 
            var activeItem = $(parent.activeItemId).widget;
            activeItem.hideItem();
        } else {
            parent.hideItem();
        }
    },
    
    open: function() {
        //console.log("site.layout.Panel.open "+this.id);
        var node = $(this.id);
        node.addClassName("panel_active");
        this._slide(1, node);
        this.isOpen = true;
    },
    
    close: function() { 
        //console.log("site.layout.Panel.close "+this.id);
        var node = $(this.id);
        node.removeClassName("panel_active");
        this._slide(0, node);
        this.isOpen = false;
    },
    
    _slide: function(state, node) { 
        //console.log("site.layout.Panel.open._slide: "+node.id + " to "+this.openpx);
        var duration, start, end, onEnd;
        if (state == 1) {
            node.style.display = "block";
            node.style.left = this.closedpx + "px";
            end = this.openpx;
            duration = this.durationOpen;
        } else {
            end = this.closedpx;
            duration = this.durationClose; 
        }
      
        new Effect.Move(node, { 
            duration: duration,
            x: end, 
            y: 0, 
            mode: 'absolute'
        });
    }
});brand.givingBack = {
	init: function() {
	    if (!page_data || site.givingBack.abort) return;
	    var section = page_data.panel_nav["default"];
	    if (section && section.id === "giving_back") {
	        // sub-section/page
	        var subsection = section.item;

	        // back to mac
	        if (subsection && subsection.id === "backtomac") {
	            site.givingBack.backToMac.init();
	        }
	    }
	}
}

brand.givingBack.backToMac = {
    init: function() {
      var popup_print = new generic.popup({
        activator: "backmac_popup",
        url: "btm_printable_form.tmpl",
        resizable: "no",
        scrollbars: "yes",
        menubar:    "yes",
        width: 750,
        height: 800
      });
    }
}

site.customerService.init = function() {
    if (!page_data || site.customerService.abort) return;
    //console.log("site.customerService.init"); 
    
    // customer service pages
    var section = page_data.panel_nav["default"];
    if (section && section.id === "customer_service" || section && section.id === "products") {
    
        // sub-section/page
        var subsection = section.item;

        // top inquiries faq & pro inquiries faq
        if (subsection && (subsection.id === "top_inquiries" || subsection.id === "pro_inquiries" || subsection.id === "pro_service")) {
            console.log("site.customerService.faq.init");
            site.customerService.faq.init();
        }

        // site map
        var maincontent = $("main_content");
        var sitemap = (maincontent ? maincontent.select('div.site-map')[0] : false);
        if ((subsection && subsection.id === "site_map") || (sitemap) ) {        
            // HACK: hide the customer service nav that automatically loads on the site map page
            // until the paths config in includes/global.tmpl is fixed
            var panel = $("panel_open");
            var nav = (panel ? panel.select('div.panel_cms_html')[0] : false);
            if (nav) {
                nav.style.display = "none";
            }           
        }
        
    }
};
site.product.init = function() { 
    if (!page_data || site.product.abort) return;
    //console.log("site.product.init ");
    
    // set rb keys
    site.product.getRBKeys();
    
    var catalog = page_data.catalog;
    
    if (catalog) {
    
        // init waitlist popover behavior
        site.product.waitlist.init();
        
        if (catalog.spp) {
            generic.events.fire({event:"PAGEDATA:RESULT",msg:"catalog.spp.product"});
            site.spp.init();
            
            // mac me overlay: for select products only
            var macMeOverlayNode = $("overlay-mac-me-over");
            if (macMeOverlayNode) site.spp.initMacMeOverlay(macMeOverlayNode);
        }
        if (catalog.mpp) { // e.g. what's new, looks
            generic.events.fire({event:"PAGEDATA:RESULT",msg:"catalog.mpp.products"});
            site.mpp.initSections();
            
            // content pages with product popovers
            if ($$(".shoppable")[0]) { 
                site.product.initShoppables({ products: catalog.mpp.products });
                return;
            }
        } 
    }
    if (page_data.video_products) { 
        site.product.videoPlayer.init();
    }
        
    // outlier pages     
    if (page_data.cms_generated && catalog) {
        if (catalog.cross_sell) {  // e.g. giving back viva glam 
            site.mpp.item.init({
                data: catalog.cross_sell
            }); 
        }
    }   
    
    // other product pages
    var section = page_data.panel_nav["default"];
    if (section && section.id === "products" && section.item) {
        // about giftcards
        if (section.item.id === "CAT792" && $("faq-answer")) {
            site.customerService.faq.init();
            
        // bloggers obsession
        } else if (section.item.id === "bloggers_obsessions") {
            site.product.initShoppables({ id: section.item.id });
        }
    }
};

site.product.getRBKeys = function () {
    site.product.rb = site.product.rb || {};
    
    // from global "language" bundle
    site.product.rb.added_to_shopping_bag = generic.rb.language.get("added_to_shopping_bag");
    //site.product.rb.items_in_bag = generic.rb.language.get("itemsinbag"); // needed?
    //site.product.rb.item_in_bag = generic.rb.language.get("item_in_bag"); // needed?
    site.product.rb.continue_shopping = generic.rb.language.get("continue_shopping");
    site.product.rb.checkout = generic.rb.language.get("checkout");
    site.product.rb.thank_you = generic.rb.language.get("thank_you");
    site.product.rb.add_to_bag = generic.rb.language.get("add_to_bag");
    site.product.rb.sorry = generic.rb.language.get("sorry");
    site.product.rb.inventory_status_message_2 = generic.rb.language.get("inventory_status_message_2");
    site.product.rb.inventory_status_message_3 = generic.rb.language.get("inventory_status_message_3");
    site.product.rb.inventory_status_message_7 = generic.rb.language.get("inventory_status_message_7");
    
    // from "brand" bundle
    var rbProduct = generic.rb("brand");
    site.product.rb["select"] = rbProduct.get("select");
    site.product.rb.to_shop = rbProduct.get("to_shop");
    site.product.rb.favorites = rbProduct.get("favorites");
    site.product.rb.added_to_favourites = rbProduct.get("was_added_to_your_favourites");
    //site.product.rb.items_in_favourites = rbProduct.get("items_in_favourites"); // needed?
    //site.product.rb.item_in_favourites = rbProduct.get("item_in_favourites"); // needed? 
    site.product.rb.limited = rbProduct.get("limited"); 
    site.product.rb.macpro = rbProduct.get("macpro");
    site.product.rb.step = rbProduct.get("step"); 
    site.product.rb.of = rbProduct.get("of");
    site.product.rb.search_results = rbProduct.get("search_shades_results");
    site.product.rb.search_no_results = rbProduct.get("search_shades_no_results");
};


site.product.initShoppables = function(args) {  
    //console.log("site.product.initShoppables");
    
    if (args.products) {
        brand.shoppableContentPD.init({ 
            products: args.products,
          
            // position popup near mouse
            positionPopup: function(evt, cartAddMsg, cartConfirmMsg) {
                var lmin = 6;
                var lmax = 530;
                var xOffset = 310; // left nav (190) & half of length of popup (120)   
                var yOffset = 100; // ~ height of popup 
                
                var t = (evt.pageY - yOffset);
                var l = (evt.pageX - xOffset); 
                // adjust for left & right edges
                if (l < lmin) {
                    l = lmin;
                } else if (l > lmax) {
                    l = lmax;
                }
                
                //console.log("t = "+t+" l = "+l);
                cartAddMsg.position = {top: t, left: l};
                cartConfirmMsg.position = {top: t, left: l};            
            }
        });
        
    } else if (args.id === "bloggers_obsessions") {   
        site.shoppableContent.init({
            containerId: "mcbo",
            overlayArgs: {
                //lockToMouse: true
                setPosition: function(linkNode, overlayNode) {
                    // use image map coords of trigger link to set position of overlay
                    var coordsArray = linkNode.coords.split(",");
                    //console.log("coordsArray = ",coordsArray);
                    var top = parseInt(coordsArray[1]);
                    var left = parseInt(coordsArray[0]);
                    //console.log("before offset: top = "+top + " left = "+left);
                    //console.log("after offset: top = "+(top + 20)+" left = "+(left-50));
                    overlayNode.style.top = (top + 30) + "px";
                    overlayNode.style.left = (left + 150) + "px"; // 190 (width of nav) - left offset since width of overlay is greater than width of shop now button
                }
            }
        });
    }
};

// extends brand.spp
if (!brand.spp) brand.spp = {};
site.spp = Object.extend(brand.spp, {

    // entity chars that should be replaced by unicode for display in select menus (e.g. "Matte&#178" finish)
    entitiesToUnicode: { 
        "&#178" : "\262",
        "&eacute;" : "é",
        "&aacute;" : "á",
        "&#233" : "é",
        "&#232" : "è",
        "&agrave" : "à",
        "&egrave" : "è"        
    },
    
    inventoryStatusNode: null,
    skuField: null,
    skuFavField: null,
        
    init: function() { 
        //console.log("site.spp.init");
        
        // SPP main product
        var is_shaded = false;
        this.skuField = skuField = $("prod_sku");
        this.skuFavField = skuFavField = $("btn_save_to_favorites");
        var prodBrowserSkuField = $("btn_color_play");
        var product = page_data.catalog.spp.product;
        this.inventoryStatusNode = $("inventory_btn_message");
        var isDiscontinued = false;
        var isSized = ((product.sized == 1 && product.skus.length > 1) ? true : false); // isSized: products with multiple skus based on size (non-shaded)
        /* TODO: distinguish between featured goodbyes products that are shoppable & unshoppabled disco prods.  Commenting out for now so that goodbyes are shoppable
        try {
            if (page_data.panel_nav["default"].item.id == "discontinued") isDiscontinued = true; 
        } catch(e) {
        }
        */

        var shadedType = null;
        var hasTabs = false;

        // solos, duos, trios+ notes:
        // solos & duos w/ 1 swatch per sku: page data "shaded" is 1
        // duos w/ 2 swatches per sku & multiple skus: data type is "other", not shaded
        // duos: sku[n]sku_multicolor_type is defined
        // "multi-colored" single skus (i.e. trio prod spp with only 1 sku containing 3 shades): page data "shaded" is 0, product.product_multicolor_type is defined
        if (product.shaded == 1 || product.product_multicolor_type) {
            // solos & multi-skued duos
            var multiShaded = false;
            if (product.shaded == 1) {
                is_shaded = true;
            }
            // check for multicolored
            var shadetype_data = brand.product.getShadeType({product: product, multicolor_min: 2}); // pass minimum # of shades to count as multi-colored
            shadedType = shadetype_data.type;
            
            // multi-shaded prods that display separate swatch images for each color value
            if ((shadetype_data.ismulti && !product.shaded)) {
                multiShaded = {};
                if (product.skus.length == 1) {
                    // skus that are 3+ multi-colored are displayed on SPP all by themselves (NOT the same as products with only 1 shoppable sku.  Former is a constant, latter is not.)
                    multiShaded.isSingleSkued = true;
                } else if (product.skus.length > 1) {
                    // multi-shaded products w/ multiple skus
                    multiShaded.isMultiSkued = true;
                }
            } 
        }

        var cartConfirmMsg = new site.product.cartConfirm({
            id: "cart_confirm_spp",
            is_shaded: is_shaded,
            isSized: isSized,
            prodName: product.name,
            nodeToReplace: $("cart_confirm_placeholder")
        });

        // show swatch container for any singles, duos, trios, etc...
        if (is_shaded || shadedType) {
            //console.log("site.product.spp: is_shaded");

            if (!multiShaded) {           
                // tab container & toggling handler
                if ($("prod-tabs")) {
                    site.spp.tabContainer.init();
                    hasTabs = true;
                }
            }
            
            // selected or default or first sku as sorted by color
            var selectedSku = page_data.selected_sku;
            if (selectedSku && selectedSku.indexOf("SKU")) { // get sku base id from full path
                selectedSku = "SKU" + selectedSku.split("SKU")[1];
            }
            if (!selectedSku) {
                if (page_data.default_sku) {
                    selectedSku = page_data.default_sku;
                } else {
                    selectedSku = product.skus[0].sku_id;
                }
            }

            // init swatches
            var swatchArgs = {
                product: product,
                skuField: skuField,
                shadedType: shadedType,
                isDiscontinued: isDiscontinued,
                selectedSku: selectedSku,
                domParent: "spp-thumbs-wrapper",
                initDefault: true
            }
            var pageArgs = {
                multiShaded: multiShaded,
                prodBrowserSkuField: prodBrowserSkuField,
                favField: skuFavField,
                cartConfirm: cartConfirmMsg,
                hasTabs: hasTabs
            };

            var swatchSet = site.spp.initSwatches({
                node: $("spp-thumbs-container"),
                swatchArgs: swatchArgs,
                pageArgs: pageArgs
            });

            // init color play link (optional)
            site.spp.initColorPlayButton(prodBrowserSkuField);
            
            // init "more" description link
            brand.spp.initDescription({
                linkNode: $("descr-full-link"), 
                descriptionNode: $("descr-full"), 
                hasDescription: product.more_desc_flag
            });
        
        } else if (isSized) {
            // isSized: products with multiple skus based on size (non-shaded)
            //console.log("site.product.spp.init: sized prod");
            site.spp.initSized({
                skus: product.skus,
                skuField: skuField,
                favField: skuFavField,
                menuNode: $("menu-sizes"),
                cartConfirmMsg: cartConfirmMsg
            });

        } else {
            // non-shaded: 1st sku is default add-to-bag button value
            // (no swatches, no menus)
            //console.log("site.product.spp.init: non-shaded");
            this.setSkuSelection({ sku: product.skus[0], cartConfirmMsg: cartConfirmMsg });
        } 
    
        // main image rollover (optional)
        site.spp.photoRollover.init(product.image_medium, product.image_medium_rollover);
        
        // unless discontinued: set up cart/favorites buttons & cross-sell items
        if (isDiscontinued) return;
        
        // add to cart button
        var sppButton = brand.product.addButton({
            addButtonNode: skuField,
            progressNode: $("progress_add_to_bag"),
            callback: function(response) {
                //console.log("site.product.sppInit sppButton callback "+cartConfirmMsg);
                cartConfirmMsg.setDisplayProperties({ type: "cart", lockToNode: $("add_to_bag"), useLeftAlign: false });
                cartConfirmMsg.show({ response: response });
            }
        });
        
        // add to favorites button
        var favSppButton = brand.product.addButton({
            addButtonNode: skuFavField,
            skuField: skuField,
            progressNode: $("progress_add_to_fav"),
            itemType: "favorites",
            callback: function(response) {
                cartConfirmMsg.setDisplayProperties({ type: "favorites", lockToNode: $("add_to_fav"), useLeftAlign: false });
                cartConfirmMsg.show({ response: response });
            }
        });
 
        // MPP cross-sell products
        site.mpp.item.init({
            data: page_data.catalog.spp.product.cross_sell
        });
           
    },
    
    initMacMeOverlay: function(overlayNode) { 
        if (!overlayNode) return;
        brand.overlay.launch({
            foregroundNode: overlayNode,
            displayInline: true,
            removeOnHide: false,
            displayDuration: 7000
        });
    }
});

if (!brand.mpp) brand.mpp = {};
site.mpp = Object.extend(brand.mpp, {
    initSections: function() { 
        //console.log("site.mpp.initSections");
        var pageContext = page_data.panel_nav["default"];
                
        if ( pageContext.item && pageContext.item.id === "looks") {
            site.mpp.initLooks();
            return;
        }

        if ( pageContext.item && page_data.catalog.subcollection_page ) {
            site.mpp.initSubcollection();
            return;                        
        }

        if ( pageContext.item && page_data.catalog.picks_page ) {
            site.mpp.initPicksCollection();
            return;                        
        }

        // favorites page
        if ( pageContext.item && pageContext.item.id === "favorites" ) {
            site.mpp.initFavorites();
            return; 
        }
    
        // Default
        site.mpp.item.init({
            data: page_data.catalog.mpp.products
        }); 
    },
    
    initLooks: function() {
   
        // buttons
        var looksSkus = page_data["all_shoppable_looks_skus"]; 
        
        if (looksSkus) {
            var skus = [];
            looksSkus.each(function(sku){
                skus.push(sku+":1");
            });
            var addAllToCart = brand.product.addButton({
                addButtonNode: $("all_to_cart_img"),
                skus: skus,
                progressNode: $("progress_all_to_cart"),
                callback: function(response) {
                    // cart confirm popover
                    brand.overlay.launch({
                        foregroundNode: $("popover-confirm-all-to-cart"),
                        displayInline: true,
                        removeOnHide: false
                    });
                }
            });
        }
        
        var products = page_data.catalog.mpp;
        for( catId in products ) {
            site.mpp.item.init({
                data: page_data.catalog.mpp[catId].products,
                initButtons: true
            });
        }   
    }, // end initLooks     

    initSubcollection: function() {
        var products = page_data.catalog.mpp;
        for( catId in products ) {
            site.mpp.item.init({
                data: page_data.catalog.mpp[catId].products,
                initButtons: true
                //altNodeId: true,
                //altType: true
            });
        }
    }, // end initSubcollection
    
    initPicksCollection: function() {
        var products = page_data.catalog.mpp;
        for( catId in products ) {
            site.mpp.item.init({
                data: page_data.catalog.mpp[catId].products,
                initButtons: true
                //altNodeId: true,
                //altType: true
            });
        }
    } // end initSubcollection
});

site.checkout.init = function() {     
    //console.log("site.checkout.init "+page_data.panel_nav["default"].id);
     
    var cartHandler = generic.checkout.cart; 
    var section = page_data.panel_nav["default"];
    
    // on signout from panel nav
    generic.events.observe("cartCount:reset", function(totalItems) {  
        cartHandler.updateCartTotals( {"totalItems": totalItems} ); //sets the Cookie in case of signout 
    });
     
    // non-checkout pages
    if (section.id !== "checkout") {  
        site.checkout.cartStatus.init();  
        cartHandler.getCartTotals();  
        return;
    }

    // suppress empty cart error on view cart page if it's the only error
    if (section.item && section.item.id === "viewcart") {
        var messages;
        var hasErrors = false;
        if (page_data.hasErrors == 1) {
            try{
                var messages = page_data.error.data.messages;
            }
            catch(err) {};
            
            // confirm that cart.empty message is the only message node in ul.error_messages
            var emptyCartErrorNode = $("cart.empty.");
            var emptyCartErrorNodeWhitespace = $(" cart.empty. "); // NOTE: hack for whitespace in rendering
            emptyCartErrorNode = emptyCartErrorNodeWhitespace || emptyCartErrorNode;
            if (messages && messages.length > 0) {
                var errorNode = $$("ul.error_messages")[0];
                var errorItems = errorNode.select("li");
                if (errorItems.length == 1 && emptyCartErrorNode) { // hide whole error block
                    errorNode.hide();
                } else if (errorItems.length > 1 && emptyCartErrorNode) { // only hide empty cart error
                    emptyCartErrorNode.hide();
                }
            }
        }
    };
    
    // checkout - all pages: "continue shopping" link
    site.checkout.makeExitBtn();

    /* MERGE NOTE: needed?
    //Checkout - all pages except Signin
    site.checkout.makeAdditionalInfoBtns(); 
    site.checkout.hasErrors = Boolean(page_data.hasErrors); 
    var canContinueCheckout = !site.checkout.hasErrors;  
    */
    
    // confirmation page: reset gnav cart total display
    if (section.item && section.item.id === "confirm") { 
        cartHandler.updateCartTotals( {"totalItems": 0} ); // resets cart cookie
    } 
};site.account.init = function() {      
    //console.log("site.account.init");  
    
    // setup generic.user
    var userParams = {};
    userParams.pageDataKey = 'nav-header.user';
    generic.user.getUser(userParams);

    // account panel nav links
    // defined in globalnav.config
    if (site.globalnav.config && site.globalnav.config.items) {
        var accountConfig = site.globalnav.config.items.find(function(item) {
            if (item.id === "account") return true;
        });
        if (accountConfig) {
            var accountSubNavConfig = accountConfig.accountnav;
            this.panel.init(accountSubNavConfig);
        }
    };
    
    var pageContext = page_data.panel_nav["default"];
    
    // account section
    if (pageContext.id !== "account") return;
    
    // messages page: display the video player if needed
    if(pageContext.item && pageContext.item.id == "messages" && $("flash_placeholder")) {
        site.product.videoPlayer.init();
    }

}; 
site.view.init = function() { 
    //console.log("site.view.init");
    if (generic.env.isIE6) site.view.setFormSelectors();
    site.view.colorNav.embed();  
    site.view.initCustomViews();
    
    site.view.utilityNav.init({ 
        minTop: 460//40px to account for additional nav items420 // minimum num px from top of body to beginning of utility nav
    });
    
    site.view.footer.adjust();  
    site.view.flashPopover.embed(); // flash popover for shipping messages, etc...
    site.view.heightHandler.init();
    site.view.initRollovers();
};  
      
site.view.initCustomViews = function() { 
    if (typeof page_data == "undefined") return;
    var panelNavDefault = page_data.panel_nav["default"];

    //artists  
    if ($("artists_block")) {
        site.view.artists.createRollOvers();
    }

    // custom palette
    if (site.pagetype === "custom-palette") {
        site.view.customPalette.init();
        return;
    } 
    
    if (panelNavDefault.item && panelNavDefault.item.id === "fromourlips") {
        site.view.fromourlips.init();
        return;
    }

    // home or shop page
    if (panelNavDefault.id === "index" || panelNavDefault.id === "home" || (panelNavDefault.item && panelNavDefault.item.id === "shop")) {
        site.view.home.init();
        return;
    }
    
    //what's new, looks, picks
    if (page_data.catalog && page_data.catalog.mpp) {
        site.view.collectionBrowser.init();
        return;
    }
    //brush play, color play, mascara finder
    if ($("productBrowser_resize")) { 
        site.view.productBrowser.init();
        return;
    }
         
    // giving_back/vivaglam:
    // add wrapper class so cross-sell modules can be controlled via external css
    if (page_data.cms_generated && page_data.catalog) {
        var maincontent = $("main_content_td");
        if (maincontent) {
            maincontent.addClassName("cms-product-content");
        }
    }
    
};

/*
 * Apply rollover to utility nav image associated with non-active region 
 */
site.view.utilityNav.initLanguageToggle = function(args) {
    var locale = args.locale;
    var localeCountry = "us";
    var suffixes = [];
    var currentLanguage = "en";
    var linkOffNode, linkOnNode;
    if (!locale) return;
    if (locale.indexOf("_ca") > -1) {
        localeCountry = "ca";
    }

    if (localeCountry === "us") {
        suffixes = ["en_us", "es_us"];
        // get current language from state of rendered links
        linkOffNode = $$("#utilitynav-toggle-region a")[0];
        if (linkOffNode && linkOffNode.id && linkOffNode.id.indexOf(suffixes[0]) > -1) {
            currentLanguage = "es";
            locale = suffixes[1];
        }
    } else {
        suffixes = ["en_ca", "fr_ca"];
    }

    var suffixOn,suffixOff;
    if (locale === suffixes[0]) {
        suffixOn = suffixes[0];
        suffixOff = suffixes[1];
    } else {
        suffixOn = suffixes[1];
        suffixOff = suffixes[0];        
    }
    linkOnNode = $("utilitynav-toggle-" + suffixOn);
    linkOffNode = linkOffNode || $("utilitynav-toggle-" + suffixOff);

    var initLinkDisplay = function() {
        if (linkOnNode && localeCountry === "ca") {
            linkOnNode.removeAttribute("href");
            linkOnNode.addClassName("unclickable");
        }
        if (!linkOffNode) return;
        var imgNode = linkOffNode.select('img')[0];
        if (imgNode) var rollover = new brand.rollover(imgNode, null);    
    };

    var initMotionPointLInk = function() {
        if (!linkOffNode) return;
        linkOffNode.observe("click", function(event) {
            if (!MP) {
                console.log("MP (MotionPoint) undefined");
                return;
            }
            event.preventDefault();
            
            var selectedLang = "es";
            if (suffixOn.indexOf("es_") > -1) {
                selectedLang = "en";
            }
            console.log("setting MP.switchLanguage to: "+selectedLang);
            MP.SrcUrl = unescape('mp_js_orgin_url');
            MP.UrlLang='mp_js_current_lang';
            MP.init();
            MP.switchLanguage(MP.UrlLang == selectedLang ? 'en': selectedLang);
            return false;       
        });
    }
    
    initLinkDisplay();
    
    if (localeCountry === "us") {
        initMotionPointLInk();
    }
    
};
/*** NAV CONFIG ***/

site.globalnav.config = {};

// default settings for items that are different in pro and/or ipad configs
site.globalnav.contextualConfig = {
    holiday: {
        name: "Holiday",
        id: "holiday",
        cmcat: "CAT794",
        header: "/images/gnav/gnav_holiday_157x18_off.gif",
        content: {
            widget: "ProductSubNav",
            url: "/cms/whats_new/panel_nav_holiday.tmpl"
        },
        template: { 
            detail: { baseClass: "panelnav_cell_category" }
        }
    },
    sceneandspotted: {
        name: "Scene and Spotted",
        id: "sceneandspotted",
        header : "/images/gnav/gnav_scenespotted_157x18_off.gif",
        uri: "/sceneandspotted/index.tmpl#/all"
    },
    bloggers: {
        name: "Bloggers' Obsessions",
        id: "bloggers_obsessions",
        header : "/images/gnav/gnav_bloggersobsessions_157x18_off.gif",
        uri: "/bloggers/"
    },
    fashionweek: {
        name: "Fashion Week",
        id: "fashionweek",
        header: "/images/gnav/gnav_fashionweek_157x18_off.gif",
        uri: "/fashionweek/"
    },

    whatsnew: { url: "/cms/whats_new/panel_nav.tmpl" },
    airbrush: null,
    studentkits: null,
    giftcards: {
        name : "Gift Card",
        id : "CAT792",
        cmcat : "CAT792",
        header : "/images/gnav/gnav_giftcard_157x18_off.gif",
        content: { url: "/cms/giftcards/panel_nav.tmpl" }
    },
    videos: {
        name: "Videos",
        id: "artistry_in_action",
        cmcat : "260",
        header : "/images/gnav/gnav_videos_157x18_off.gif",
        content: {
            widget: "VideosSubNav",
            url: "/cms/makeup_artistry/artistry_action/panel_nav.tmpl",
            featuredMax: 5 // number of videos to show in featured (initial display) list
        }           
    }
};

// pro-specific settings 
site.globalnav.contextualConfigPro = {
    holiday: {
        name: "Holiday",
        id: "holiday",
        cmcat: "CAT794",
        header: "/images/gnav/gnav_holiday_157x18_off.gif",
        content: {
            widget: "ProductSubNav",
            url: "/cms/whats_new/panel_nav_holiday_pro.tmpl"
        },
        template: { 
            detail: { baseClass: "panelnav_cell_category" }
        }
    },
    sceneandspotted: site.globalnav.contextualConfig.sceneandspotted,
    bloggers: site.globalnav.contextualConfig.bloggers,
    fashionweek: site.globalnav.contextualConfig.fashionweek,
    whatsnew: { url: "/cms/whats_new/panel_nav_pro.tmpl" },
    airbrush: {
        name : "Airbrush",
        id : "CAT2780",
        cmcat : "CAT2780",
        header : "/images/gnav/gnav_airbrush_157x18_off.gif"
    },
    studentkits: null,
    giftcards: null,
    videos: site.globalnav.contextualConfig.videos
};

// pro-student-specific settings 
site.globalnav.contextualConfigProStudent = {
    holiday: site.globalnav.contextualConfigPro.holiday,
    sceneandspotted: site.globalnav.contextualConfig.sceneandspotted,
    bloggers: site.globalnav.contextualConfig.bloggers,
    fashionweek: site.globalnav.contextualConfig.fashionweek,
    whatsnew: { url: "/cms/whats_new/panel_nav_pro.tmpl" },
    airbrush: site.globalnav.contextualConfigPro.airbrush,
    studentkits: {
        name : "Student Kits",
        id : "CAT2781",
        cmcat : "CAT2781",
        header : "/images/gnav/gnav_studentkits_157x18_off.gif"
    },
    giftcards: null,
    videos: site.globalnav.contextualConfig.videos
};

// ipad-specific settings 
site.globalnav.contextualConfigIPad = {
    holiday: site.globalnav.contextualConfig.holiday,
    sceneandspotted: site.globalnav.contextualConfig.sceneandspotted,
    bloggers: site.globalnav.contextualConfig.bloggers,
    fashionweek: site.globalnav.contextualConfig.fashionweek,
    whatsnew: site.globalnav.contextualConfig.whatsnew,
    airbrush: site.globalnav.contextualConfig.airbrush,
    studentkits: site.globalnav.contextualConfig.studentkits,
    giftcards: site.globalnav.contextualConfig.giftcards,
    videos: null
};


// Returns final set of config items based on site status 
// Runs after page (page_data) has loaded so pro status is known
site.globalnav.getConfig = function() {
    var contextualConfig = site.globalnav.contextualConfig;
    if (global.isprostudent) {
        contextualConfig = site.globalnav.contextualConfigProStudent;
    } else if (global.ispro) {
        contextualConfig = site.globalnav.contextualConfigPro;
    } else if (global.isipad) {
        contextualConfig = site.globalnav.contextualConfigIPad;
    }
    
    var isESlocale = ((page_data && page_data.is_es_us_locale == 1) ? true : false);
    
    var config = {
        items : [
            {
                name: "Shop Products",
                id: "products",
                parentId: "globalnav_container",
                domParent: "globalnav",
                header: "/images/gnav/gnav_products_157x18_off.gif",
                 
                // supercategories: shared attributes
                itemsConfig: {
                    content: {
                        widget: "ProductSubNav",
                        url: "/includes/panel_nav/catalog.tmpl",
                        param: "CATEGORY_ID"
                    }
                },
                // supercategories: data
                items : [
                    //contextualConfig.holiday,
                    //contextualConfig.sceneandspotted,
                    //contextualConfig.bloggers,
                    //contextualConfig.fashionweek,
                    {
                        name : "What's New",
                        id : "whatsnew",
                        cmcat : "CAT794",
                        header : "/images/gnav/gnav_whatsnew_157x18_off.gif",
                        content: {
                            widget: "ProductSubNav",
                            url: contextualConfig.whatsnew.url
                        },
                        template: { 
                            detail: { baseClass: "panelnav_cell_category" }
                        }
                    },
                    {
                        name : "Eyes",
                        id : "CAT148",
                        cmcat : "CAT148",
                        header : "/images/gnav/gnav_eyes_157x18_off.gif"
                    },
                    {
                        name : "Lips",
                        id : "CAT163",
                        cmcat :  "CAT163",
                        header : "/images/gnav/gnav_lips_157x18_off.gif"
                    },
                    {
                        name : "Face",
                        id : "CAT155",
                        cmcat : "CAT155",
                        header : "/images/gnav/gnav_face_157x18_off.gif"
                    },
                    {
                        name : "Mineralize",
                        id : "CAT869",
                        cmcat :"CAT869",
                        header : "/images/gnav/gnav_mineralize_157x18_off.gif"
                    },
                    {
                        name : "Multi-use",
                        id : "CAT793",
                        cmcat : "CAT793",
                        header : "/images/gnav/gnav_multiuse_157x18_off.gif"
                    },
                    {
                        name : "Skincare",
                        id : "CAT176",
                        cmcat : "CAT176",
                        header : "/images/gnav/gnav_skincare_157x18_off.gif"
                    },
                    {
                        name : "Brushes",
                        id : "CAT144",
                        cmcat : "CAT144",
                        header : "/images/gnav/gnav_brushes_157x18_off.gif"
                    },
                    {
                        name : "Nails",
                        id : "CAT170",
                        cmcat : "CAT170",
                        header : "/images/gnav/gnav_nails_157x18_off.gif"
                    },
                    {
                        name : "Fragrance",
                        id : "CAT161",
                        cmcat : "CAT161",
                        header : "/images/gnav/gnav_fragrance_157x18_off.gif"
                    },
                    {
                        name : "Kit Essentials",
                        id : "CAT133",
                        cmcat : "CAT133",
                        header : "/images/gnav/gnav_kit_essentials_157x18_off.gif"
                    },
                    {
                        name : "Accessories",
                        id : "CAT139",
                        cmcat: "CAT139",
                        header : "/images/gnav/gnav_accessories_157x18_off.gif"
                    },
                    contextualConfig.airbrush,
                    contextualConfig.studentkits,
                    {
                        name : "Goodbyes",
                        id : "discontinued",
                        cmcat : "CAT20833",
                        header : "/images/gnav/gnav_discontinuedproducts_157x18_off.gif",
                        content: { 
                            url: "/discontinued/panel_nav.tmpl",
                            param: "dquery",
                            // goodbyes data: should come from panel nav include
                            featured : {
                                "name" : "Featured Goodbyes", 
                                "id" : "featured_goodbyes",
                                "header" : "/images/goodbyes/pnav/pnav_featured_goodbyes_250x18_off.gif",
                                "uri" : "/products/featured_goodbyes.tmpl"
                            }
                        },
                        template: {
                            detail: {
                                type: "jsTemplates.globalnav.SimpleDetail",
                                baseClass: "panelnav_cell_category"
                            }
                        },
                        search: {
                            formFieldId: "disc_search_input",
                            formSubmitId: "disc_search_submit"
                            //errorPopup: "pop_search_invalid"
                        }
                    },
                    contextualConfig.giftcards,
                   
                    {
                        name : "Studio",
                        id : "CAT1921",
                        cmcat: "CAT1921",
                        suppressGnavLink : true // has a panel nav but no link in gnav
                    }
                ]
            }, // end products
          
            {
                name: "Makeup Artistry",
                id: "makeup_artistry",
                parentId: "globalnav_container",
                domParent: "globalnav",
                header: "/images/gnav/gnav_artistry_157x18_off.gif",
                
                items : [
                    {
                        name: "News",
                        id: "newsworthy",
                        header : "/images/gnav/gnav_newsworthy_157x18_off.gif",
                        uri: "/makeup_artistry/newsworthy.tmpl"
                    },
                    {
                        name: "The Artists",
                        id: "artists",
                        cmcat : "CAT1048",
                        header : "/images/gnav/gnav_theartists_157x18_off.gif",
                        content: { 
                            url: "/cms/makeup_artistry/artists/panel_nav.tmpl",
                            handleAs: "html",
                            cms: true // global nav writes out cms panel navs for default state
                        }         
                    },
                    contextualConfig.videos,
                    {
                        name: "Email An Artist",
                        id: "email_an_artist",
                        cmcat: "1300",
                        header : "/images/gnav/gnav_email_an_artist_157x18_off.gif",
                        content: { url: "/cms/makeup_artistry/email_artist/panel_nav.tmpl" },
                        template: { 
                            detail: { type: "jsTemplates.globalnav.SimpleDetail" }
                        }
                    },
                    {
                        name: "MAC Pro",
                        id: "macpro",
                        cmcat: "280",
                        header : "/images/gnav/gnav_macpro_157x18_off.gif",
                        content: { 
                            url: "/cms/makeup_artistry/mac_pro/panel_nav.tmpl",
                            handleAs: "html",
                            cms: true
                        }
                    }
                ]
            }, // end makeup_artistry
    
            {
                name: "Giving Back",
                id: "giving_back",
                cmcat: "290",
                parentId: "globalnav_container",
                domParent: "globalnav",
                header: "/images/gnav/gnav_givingback_157x18_off.gif",          
                content: { url: "/cms/giving_back/panel_nav.tmpl" },
                template: { 
                    detail: {
                        type: "jsTemplates.globalnav.SimpleDetail",
                        baseClass: "panelnav_cell_category"
                    }
                }
            },
    
            {
                name: "My Mac",
                id: "account",
                cmcat: "1000",
                parentId: "globalnav_container",
                domParent: "globalnav",
                header: "/images/gnav/gnav_mymac_157x18_off.gif",
                // subnav settings
                content: {
                    hasLoaded: true, // subnav already placed into page
                    handleAs: "html",
                    cache: false,
                    reinsertNode: true
                },
                "accountnav": {
                    sections: ["account_index", "member_registration", "registration", "address_book", "payment_info", "order_history", "purchases", "favorites", "reviews", "messages"]
                }
            },
    
            {
                name: "Customer Service",
                id: "customer_service",
                cmcat: "1400",
                parentId: "globalnav_container",
                domParent: "utilitynav_links",
                header: "/images/gnav/gnav_customer_service.gif",
                content: {
                    url: (isESlocale ? "/customer_service/panel_nav_es_us.tmpl" : "/cms/customer_service/panel_nav.tmpl"), // EN_US only setting for ES version of CS nav
                    handleAs: "html",
                    cms: true
                }
            }, 
    
            {
                id: "search",
                cmcat: "1700",
                content: { 
                    url: "/search/includes/panel_nav.tmpl",
                    param: "query"
                },
                // for product search in includes/global_nav.tmpl
                search: {
                    formFieldId: "search",
                    formSubmitId: "search_button",
                    errorPopup: "pop_search_invalid"
                }
            }
        ],
    
        
        // key for handling non-default content types as passed in data as item.type (via loader config or via included tmpl JSON)
        // EX: collections Detail modules use default baseClass "panelnav_cell_category" as well as "panelnav_cell_header_only"
        altTypes : {
            "header_only" : { 
                detail: { baseClass: "panelnav_cell_header_only" }
            },
            "simple_detail" : {
                detail: { template: "SimpleDetail" }
            }
        }
        
    }; // end config
   
    // notify analtyics this is ready
    document.fire('sitenav:loaded',config); // TODO: change to "generic.events.fire"
 
    return config;
};
/*
  Global nav init methods for all NA locales
*/

// initialize globalnav 
site.globalnav.init = function() { 
    if (site.globalnav.abort || !$("globalnav")) return;
    //console.log("site.globalnav.init "+page_data.panel_nav["default"].id);
    
    var config = site.globalnav.config = site.globalnav.getConfig();
    var section = page_data.panel_nav["default"].id;

    // get default page state 
    // structure: defaultState{ id: "", item: { id: "", item: {...} } }
    var defaultState = {};
    if (page_data && page_data.panel_nav) {
        defaultState = page_data.panel_nav["default"];
    }
    
    if (section === "products") {
        $("panel_open").addClassName("panel_open_products_panel");     
    }
    
    // in global_nav.tmpl
    globalNavSetId = "globalnav_container"; 
      
    // init entire nav
    var globalSet =  new site.globalnav.GlobalSet({id: globalNavSetId});
    globalSet.gnav = new site.globalnav.GlobalNav({
        config: config, // all nav items
        defaultState: defaultState, // default/open state data
        globalNavSetId: globalNavSetId
    });

    // after globalnav renders
    // sets reveal of content that's supposed to be hidden until gnav loads
    // hidden content specified by "hide-before-globalnav-load" css class
    if (!config.items) return;  
    var nodesToShow = $$(".hide-before-globalnav-load"); 
    nodesToShow.each(function(node) {
        node.removeClassName("hide-before-globalnav-load");
    });
    
    // init rollovers for whats new
    // MAC ME OVER only currently
    site.globalnav.collectionThumbnailRollovers.init({ ids: ["CAT6411", "CAT6412", "CAT6413"] });

};


// custom rollover states for collection panel nav thumbnails
site.globalnav.collectionThumbnailRollovers = {
    isInitialized: false,
    init: function(args) {
        var ids = args.ids;
        var self = this;
            
        var initRollover = function(imgNode, id) {
            //console.log("initRollover");
            var outImg = imgNode.src;
            var linkNode = $$("#psubitem_"+id+" A")[0]; // workaround for IE: get inner A element to attach event to since IE throws error on event handler for outer link element
            if (!outImg || !linkNode) return;
            var imgPath = outImg.split(".jpg")[0];
            if (imgPath) imgPath = imgPath.split("://")[1]; // filter out domain, so US ES file requests don't get funky.
            if (imgPath) imgPath = imgPath.split("/images/")[1]; // not assuming .com in domain so assume /images/ path instead
            var overImg = "/images/" + imgPath + "_alt." + "jpg"; // naming convention for images: xxx.jpg & xxx_alt.jpg. NOTE: "alt." & "jpg" not concatenated to avoid ES MP servers from replacing "_alt.jpg" with domain+"_alt.jpg"
            if (!overImg) return;
            linkNode.outImg = outImg;
            linkNode.imgNode = imgNode;
            linkNode.isOver = false;
            var overImgObj = new Image();
            overImgObj.src = overImg;
            linkNode.overImgObj = overImgObj;
           
            var over = function(e) {
                this.isOver = true;
                if (this.imgNode) this.imgNode.src = this.overImgObj.src; // this = event target node
            };
            
            var out = function(e) {
                var eventObj = this;
                var doOut = function() {
                    if (!generic.env.isIE || (generic.env.isIE && !eventObj.isOver)) {
                        if (eventObj.imgNode) eventObj.imgNode.src = eventObj.outImg;
                    }
                }
                // IE workaround for rollover flickering. cancel out animation unless mouse is totally out of item
                if (generic.env.isIE) {
                    eventObj.isOver = false;
                    doOut.delay(0.2);
                } else {
                    doOut();
                }
            };
            
            linkNode.observe("mouseover", over);
            linkNode.observe("mouseout", out);
        };
        
        // listen for collection panel nav loading event
        generic.events.observe("panelnav:contentloaded", function(event) {
            if (event && (event.itemId === "whatsnew")) {
                if (self.isInitialized) return;
                ids.each(function(id, idx) {
                    var imgNode = $("psubitem_"+id+"_thumb");
                    if (imgNode) initRollover(imgNode, id);
                });
                self.isInitialized = true;
            }
        });
    }
};
//
// EndecaCatalog class.
// This class parses the Endeca JSON data into category, product, and sku objects,
// and maintains lists (hashes) for each type.  You can then access each list by id,
// such as:
//		var catObj = this.categoryList[catid];
// The structure of the lists/objects is essentially the same as the "CatProdData" class
// of the CL-US project.  Thus, each product object also contains a "skus" list which
// is a list of references to sku objects (also on the skuList) which are skus for that
// product.  Thus, when you get a product object from the productList, that product
// object will have a list of all it's skus (at least the skus that were included
// in the data).
//
// There is a "rec_type" property on each data record that indicates either "product"
// or "content".  "product" record types have cat/prod/sku data, and are stored as such.
// "content" records are not products, and the data for these records is stored
// essentially verbatim on the "contentList" hash.  Typically content records are
// things like articles and videos on the site.
//
// NOTE - this class is intended only to be a convenient container for the data
// returned from an Endeca query.  Please do not add page/state data to this class.
//
// A discussion of the Endeca data format is at the end of this file.
//	

var EndecaCatalog = Class.create({

	initialize: function(args) {
		
		this.categoryList = {};
		this.productList = {};
		this.skuList = {};
		this.contentList = $A();
		this.responseError = '';
		this.jsonResult = null;
		this.parseOrderHi = 0;
		this.parseOrderLo = 0;
		this.insert = false;
		
		// Load up passed params
        Object.extend(this, args || {});
        
        // If we had data given to us, handle it now.
        if ( this.jsonResult ) {
        	this.parseData(this.jsonResult,this.insert);
        }
	},
	
	parseData: function(jsonResult,insert) {
	  	// Check for errors
	  	if ( jsonResult.methodResponse &&
			 jsonResult.methodResponse.fault &&
			 jsonResult.methodResponse.fault.value &&
			 jsonResult.methodResponse.fault.value.faultString ) {
			
			this.responseError = jsonResult.methodResponse.fault.value.faultString;
	  	}
	  	
	  	if ( jsonResult.AggrRecords ) {
		  	// Dig out the aggregated records.
		  	// This actually merges the new records into our existing catalog, if present.
		  	jsonResult.AggrRecords.each ( function(aggrRec) {
		  		
		  		// Handle the main aggregate properties.
		  		// This is a "sample" sku record.
		  		this.parseERecord(aggrRec,insert);
		  		
		  		// All sub-records represent skus.
		  		aggrRec.Records.each ( function(skuRec) {
		  			this.parseERecord(skuRec,insert);
		  		}, this);
		  	}, this);
		} else if ( jsonResult.Records ) {
			jsonResult.Records.each ( function(skuRec) {
	  			this.parseERecord(skuRec,insert);
			}, this);
		}
	},
	
	parseERecord: function(endecaRecord,insert) {
		
		// If the record type is not "product", we assume it's
		// some kind of content and just save the whole record.
		// Else we'll pull it apart into cat/prod/sku data
		var recType = endecaRecord.Properties["rec_type"];
		if ( recType != 'product' ) {
			var recId = endecaRecord.Properties["rec_id"];
			if ( recId ) {
				//var recObj = this.contentList[recId] || {};
				//this.contentList[recId] = Object.extend(recObj,endecaRecord);
				
				this.contentList.push({
				    "image" : '',
				    "header_text" : endecaRecord.Properties.p_PROD_RGN_NAME,
				    "description" : endecaRecord.Properties.p_DESCRIPTION,
				    "link_url" : endecaRecord.Properties.p_url,
				    "link_text" : 'View more &raquo;'
                });
			}
		} else {
			// Must be a product...
	
			// The "Properties" hash is a list off all fields of a sku.
			// It actually contains cat, prod, and sku properties, each
			// of which is prefixed with "c_", "p_", and "s_", so we can pull them out.
			var catProps = {};
			var prodProps = {};
			var skuProps = {};
			
			// This is a list of properties that we always want to force to an integer value.
			// Note - in theory, this should be settable in the export process... TODO...
			var intProps = {
				"DISPLAY_ORDER":true,
				"shaded":true,
				"sized":true,
				"GIFTWRAP":true,
				"HAZARDOUS_PRODUCT":true,
				"REFILLABLE":true,
				"SUPRESS_IN_CART":true,
				"ONLY_RATINGS_COUNT":true,
				"PRODUCT_TYPE": true,
				"SKIN_TONE": true,
				"SKIN_TYPE": true,
				"TOTAL_REVIEW_COUNT": true,
				"INVENTORY_STATUS": true,
				"MISC_FLAG": true,
				"shoppable": true
			};
			
			// Props to force to float values
			var floatProps = {
				"AVERAGE_RATING": true
			};
			
			prodProps['matched'] = 1;
			skuProps['matched'] = 0;
			
			var splitRec = function(rec) {
				var key = rec.key.substr(2);
				var val = ( intProps[key] ? parseInt(rec.value) : 
				 			floatProps[key] ? parseFloat(rec.value) :
							rec.value );
				
				//Set up product tabs
				
				if (rec.key == 'p_tab_json' && val != "") {
				    prodProps['tabs'] = val.evalJSON();
				}
				
				if ( rec.key.indexOf ( "c_" ) == 0 ) {
					catProps[key] = val;
				} else if ( rec.key.indexOf ( "p_" ) == 0 ) {
					prodProps[key] = val;
				} else if ( rec.key.indexOf ( "s_" ) == 0 ) {
					skuProps[key] = val;
				} else if ( rec.key == "DGraph.WhyDidItMatch" ) {
                    skuProps['matchedOn'] = rec.value;
                    
                    [rec.value].each( function(matchedOn){
                        if ( matchedOn.indexOf ( "s_" ) == 0 ) {
    				        prodProps['matched'] = 0;
    				        skuProps['matched'] = 1;
    				    } 
                    });
				}
			};
			
			$H(endecaRecord.Properties).each ( splitRec, this);
			$H(endecaRecord.Dimensions).each ( splitRec, this);
			
			// Save the Endeca-specific info we care about.
			// First, do sku info.
			['Record Spec','Dimensions','Record Detail Link'].each ( function(prop) {
				skuProps[prop] = ( endecaRecord[prop] ? endecaRecord[prop] : '' );
			});
			
			// Check product props
			['Record Count','AggRec Detail Link'].each ( function(prop) {
				prodProps[prop] = ( endecaRecord[prop] ? endecaRecord[prop] : '' );
			});
			
			this.addProps ( catProps, prodProps, skuProps, insert );
		}
	},
	
	addProps: function ( catProps, prodProps, skuProps, insert ) {
		// For now, i'm using id's, but we may want to use the "path", which is more specific...
		var catId = catProps.CATEGORY_ID;
		var prodId = prodProps.PRODUCT_ID;
		var skuId = skuProps.SKU_ID;
		
		// I'm paranoid - check for id's
		if ( !catId || !prodId || !skuId ) return;
		
		// Insert/update sku object
		var skuObj = this.skuList[skuId] || {};
		this.skuList[skuId] = Object.extend(skuObj,skuProps);
		
		// If existing product record, use that and update.
		// Else create new one.
		var prodObj = this.productList[prodId] || { parseOrder: ++this.parseOrderHi };
		
		// If inserting, parse order should be negative.
		if ( insert && prodObj.parseOrder > 0 ) {
			prodObj.parseOrder = --this.parseOrderLo;
		}
		
		prodObj = Object.extend(prodObj,prodProps);
		if ( !prodObj.skus )
			prodObj.skus = [];
		if ( !prodObj.skuList )
			prodObj.skuList = {};
		// Make sure each sku is listed only once per product
		if ( !prodObj.skuList[skuId] ) 
			prodObj.skus.push(skuObj);
		prodObj.skuList[skuId] = skuObj;
		this.productList[prodId] = prodObj;
		
		var catObj = this.categoryList[catId] || {};
		catObj = Object.extend(catObj,catProps);
		if ( !catObj.prods ) catObj.prods = [];
		catObj.prods.push(prodObj);		
		this.categoryList[catId] = catObj;
	},
	
	// Return an array of product objects, sorted by parseOrder
	getProducts: function() {
		var prods = [];
		$H(this.productList).sortBy( function(ps) { 
			return ps.value.parseOrder; 
		}).each ( function(p) {
		    var skus = [];
		    p.value.skus.sortBy(function(ss) {
		        return ss.DISPLAY_ORDER;
            }).each(function(s) {
                skus.push(s);
            });
            p.value.skus = skus;
            
            prods.push(p.value); 
		});
		/*
		$H(this.productList).each( function(p) {
			prods.push(p.value); 
		});
		*/
		
		return prods;
	},
	
	// Return an array of sku objects
	getSkus: function() {
		var skus = [];
		$H(this.skuList).each( function(s) {
			skus.push(s.value);
		});
		return skus;
	},
	
	getCategory: function(catid) {
		var catObj = ( this.categoryList ? this.categoryList[catid] : null );
		return catObj;
	},
	
	getProduct: function(prodid) {
		var prodObj = ( this.productList ? this.productList[prodid] : null );
		return prodObj;
	},
	
	getSku: function(skuid) {
		var skuObj = ( this.skuList ? this.skuList[skuid] : null );
		return skuObj;
	},
	
	productCount: function() {
		return $H(this.productList).size();
	},
	
	contentCount: function() {
	    this.contentList.size();
		//return $H(this.contentList).size();
	},
	
	recordCount: function() {
		return this.productCount() + this.contentCount();
	}
	
});


/*
The Endeca data arrives as a JSON hash.  Each record in the hash is a complete sku record,
including all the category, product, and sku properties for that sku.

When we request the data, we request a rollup on PRODUCT_ID.  This groups the
data by product.  The "parent" product is called the "Aggregate Record" (in Endeca-land).
Thus, in the hash, the first sku is the "AggrRecord" (which is a sku representative of
the rolled-up product record), and all the other skus (if any) then follow that record.

Because Endeca flattens the entire database into sku-specific records, before sending
the data to Endeca, we pre-pend each property name with a "c_", "p_", or "s_", to indicate
if that property is a Category, Product, or Sku property (respectively). Then, when
we parse out each Endeca record, we can split the record into category, product, and
sku properties.  As we parse the data, we create a category, product, and sku object
for each sku, and then merge that object into the list of cats/prods/skus in our
EndecaCatalog class.

Note that because a product often has multiple skus, we will see the same product id
more than once as we process those skus.  Thus, we want to "update" the product record
in our list with the new sku info, and not just "add" the product record (since that would
potentially create duplicate product records).  A similar scenario may occur if a
product belongs to more than one category.

Here is a very simplified synopsys of the Endeca data JSON format:

	AggrRecords: [
		{	-- new product
			Records: [
				{
					Properties: {
						c_*
						p_*
						s_*
					}
				}
				{
					Properties: {
						c_*
						p_*
						s_*
					}
				}
			]
		}
		{	-- new product
			Records: [
				{
					Properties: {
						c_*
						p_*
						s_*
					}
				}
				{
					Properties: {
						c_*
						p_*
						s_*
					}
				}
			]
		}
	]
*/
	
//
// EndecaMeta class.
// This provides a convenience wrapper for parsing the metadata of Endeca results.
// After parsing, we can access the properties we care about as valid js properties.
// There is no other use for this class (please don't add page/state specific
// code in this module).
//

var EndecaMeta = Class.create({
	
	initialize: function(args) {
		
		this.metaInfo = $H();
		this.refinements = $A();
		this.breadcrumbs = $A();
		this.supplemental = $A();
		this.searchInfo = $H();
		this.jsonResult = null;
		
		// Load up passed params
        Object.extend(this, args || {});
        
        // If we had data given to us, handle it now.
        if ( this.jsonResult ) {
        	this.parseData(this.jsonResult);
        }
	},
	
	parseData: function(jsonResult) {
  		this.metaInfo = ( jsonResult.MetaInfo ? $H(jsonResult.MetaInfo) : this.metaInfo );
  		this.refinements = ( jsonResult.Refinements ? $A(jsonResult.Refinements) : this.refinements );
  		this.breadcrumbs = ( jsonResult.Breadcrumbs ? $A(jsonResult.Breadcrumbs) : this.breadcrumbs );
  		this.supplemental = ( jsonResult["Supplemental Objects"] ? $A(jsonResult["Supplemental Objects"]) : this.supplemental );
  		this.searchInfo = ( jsonResult["Search Info"] ? $H(jsonResult["Search Info"]) : this.searchInfo );
  		
  		this.searchTerm = ( jsonResult["Search Info"] && 
  							jsonResult["Search Info"]["all"] && 
  							jsonResult["Search Info"]["all"]["Search Term"]  ? 
  							jsonResult["Search Info"]["all"]["Search Term"] : '' );
  		
  		// Check for spell correction					
		this.searchCorrected = '';
  		var arTerms = 
  						( jsonResult["Search Info"] && 
  							jsonResult["Search Info"]["all"] && 
  							jsonResult["Search Info"]["all"]["Spell Correction"]  ? 
  							$A(jsonResult["Search Info"]["all"]["Spell Correction"]) : null );
  		if ( arTerms ) {
  			arTerms.each( function(el) {
  				// If we matched on a computed phrase AFTER the spell correction,
  				// then the "Spell Correction" flag is not set properly.
  				// Also, the "New Term" will have quotes around it, which further
  				// messes up the comparison.  So... to test for spell correction,
  				// see if the user's search term is NOT within the corrected term.
  				//if ( el["New Term"] && el["Spell Correction"] == "true" ) {
  				if ( el["New Term"] && el["New Term"].indexOf(this.searchTerm) < 0 ) {
  					if ( this.searchCorrected.length > 0 ) this.searchCorrected += ',';
  					this.searchCorrected += el["New Term"];
  				}
  			},this);
  		}
  		
  		// Check for did-you-mean
  		this.didYouMean = '';
  		this.didYouMeanLink = '';
  		arTerms = 
  						( jsonResult["Search Info"] && 
  							jsonResult["Search Info"]["all"] && 
  							jsonResult["Search Info"]["all"]["DYM Information"]  ? 
  							jsonResult["Search Info"]["all"]["DYM Information"] : null );
  							
  		// Note - we're only going to handle one dym term...
  		// Quirk: With computed phrases turned on, if the user enters
  		// an exact phrase that we have in our search config, we'll get
  		// a dym entry of the quoted phrase.  Since this is confusing and unnecessary,
  		// we'll just skip those dym's.
  		if ( arTerms && arTerms[0] && arTerms[0]["New Term"] && arTerms[0]["Pivot Link"] &&
  		     arTerms[0]["New Term"].indexOf(this.searchTerm) < 0 ) {
  			this.didYouMean = arTerms[0]["New Term"];
  			this.didYouMeanLink = arTerms[0]["Pivot Link"];
  		}
  		
  		// Parse supplemental object for:
  		//  - keyword redirect
  		//  - merch window top banner
  		//	- featured products
  		//  - content results
  		this.keywordRedirect = '';
  		this.mwBannerTop = '';
  		this.arFeaturedProducts = $A();
  		this.contentList = $A();
  		this.topContentBanner;
  		if ( this.supplemental ) {
  			this.supplemental.each(function(elSup){
  				// elSup is a hash with (perhaps) Dimensions and Properties
  				if ( elSup["Properties"] ) {
  					
  					if ( elSup["Properties"]["DGraph.KeywordRedirectUrl"] ) {
  						this.keywordRedirect = elSup["Properties"]["DGraph.KeywordRedirectUrl"];
  					}
  					
  					// "Zone" is something we set up in the Endeca Dev Studio.
  					// It's fairly hard-coded into the Endeca config.
  					// We defined several zones, which correspond to the
  					// areas on the search page that we want to put stuff.
  					
  					// DGraph.SeeAlsoMerchId is an Endeca property.
  					// m_merchwin_banner_top is ours, created in the Merch Workbench.
  					if ( elSup["Properties"]["DGraph.SeeAlsoMerchId"] &&
  					     elSup["Properties"]["Zone"] == "Top Offer Banner" ) {
  					  	this.mwBannerTop =
  					  		( elSup["Properties"]["m_merchwin_banner_top"] ? elSup["Properties"]["m_merchwin_banner_top"] :
  					  		  elSup["Properties"]["banner_template"] ? elSup["Properties"]["banner_template"] :
  					  		  '' );
  					}
  					
  					// See if there are featured products.
  					if ( elSup["Properties"]["DGraph.SeeAlsoMerchId"] &&
  					     elSup["Properties"]["Zone"] == "Featured Product Banner" &&
  					     elSup["Records"] ) {
  						// There are featured products.  Go dig them out of the record specs.
  						$A(elSup["Records"]).each( function(eFeatRec){
  							if ( eFeatRec["Record Spec"] ) {
  								this.arFeaturedProducts.push(eFeatRec["Record Spec"]);
  							}
  						},this);
  					}
  					
  					
  					// See if there top content banner results and regular content results.
  					var locale = generic.cookie('LOCALE');
  					var zone_suffix = locale == "fr_CA" ? "French" : "English";
  					
  					if ( elSup["Properties"]["DGraph.SeeAlsoMerchId"] &&
  					     elSup["Properties"]["Zone"] == "Top Content Banner " + zone_suffix &&
  					     elSup["Properties"]["Style"] == "content result" ) {
  						
  						if ( ( global.ispro && elSup["Properties"]["suppress_pro"] ) || elSup["Properties"]["suppress_" + locale] ) {
  						    
  						} else {
                            this.topContentBanner = {
    						    "image" : elSup["Properties"]["image"],
    						    "header_image_url" : elSup["Properties"]["header_image_url"],
    						    "header_text" : elSup["Properties"]["header_text"],
    						    "description" : elSup["Properties"]["description"],
    						    "link_url" : elSup["Properties"]["link_url"],
    						    "link_text" : elSup["Properties"]["link_text"],    						    
    						    "css_class" : elSup["Properties"]["css_class"]
                            };
                        }
  					}
  					
  					if ( elSup["Properties"]["DGraph.SeeAlsoMerchId"] &&
  					     elSup["Properties"]["Zone"] == "Content Results " + zone_suffix &&
  					     elSup["Properties"]["Style"] == "content result" ) {
  						
  						if (global.ispro && elSup["Properties"]["suppress_pro"]) {
  						    
  						} else {
                            this.contentList.push({
    						    "image" : elSup["Properties"]["image"],
    						    "header_image_url" : elSup["Properties"]["header_image_url"],
    						    "header_text" : elSup["Properties"]["header_text"],
    						    "description" : elSup["Properties"]["description"],
    						    "link_url" : elSup["Properties"]["link_url"],
    						    "link_text" : elSup["Properties"]["link_text"],
    						    "css_class" : elSup["Properties"]["css_class"]
                            });
                        }
  					}
  				}
  			},this);
  		}
  		
  		// Parse out some specifics for convenience
  		this.numberOfPages       = this.getMetaProp ( "Number of Pages" , 1, true );
  		this.numberOfCurrentPage = this.getMetaProp ( "Page Number" , 1, true );
  		this.directPageLinkStart = this.getMetaProp ( "Direct Page Link Start Page", 1, true );
  		this.directPageLinks     = this.getMetaProp ( "Direct Page Links", [] );
  		this.nextPageLink        = this.getMetaProp ( "Next Page Link", '' );
  		this.prevPageLink        = this.getMetaProp ( "Previous Page Link", '' );
  		this.nextPageSetLink     = this.getMetaProp ( "Next Page Set Link", '' );
  		this.prevPageSetLink     = this.getMetaProp ( "Previous Page Set Link", '' );
  		this.startingRecord      = this.getMetaProp ( "Starting Record Number", 1, true );
  		this.endingRecord        = this.getMetaProp ( "Ending Record Number", 1, true );
  		this.recordsReturned     = this.getMetaProp ( "Number of Records Returned", 0, true );
  		this.recordsPerPage      = this.getMetaProp ( "Number of Records per Page", 0, true );

  		this.totalMatchRecords   = this.getMetaProp ( "Total Number of Matching Records", 0, true );
  		this.totalAggrRecords    = this.getMetaProp ( "Total Number of Matching Aggregate Records", 0, true );
  		this.totalRecords        = ( this.totalAggrRecords ? this.totalAggrRecords : this.totalMatchRecords );
	},
	
	// Convenience function to look for prop and return default if not found
	getMetaProp: function(prop,def,isNum) {
		var val = ( this.metaInfo.get(prop) ? this.metaInfo.get(prop) : def );
		if ( isNum ) {
			val = parseInt(val);
		}
		return val;
	}
	
});

/*
"MetaInfo" : {
	"Number of Records per Page" : "10",
	"Number of Records Returned" : "10",
	"Starting Record Number" : "11",
	"Ending Record Number" : "20",
	"Total Number of Matching Records" : "2937",
	"Total Number of Matching Aggregate Records" : "1363",

	"Add Sort Key Links" : [
	],
	"Total Network and Compute Time" : "0.01",

	"Page Number" : "2",
	"Number of Pages" : "137"
	"Next Page Link" : "N=&Nao=20&Np=2?=p_PRODUCT_ID",
	"Previous Page Link" : "N=&Nao=0&Np=2?=p_PRODUCT_ID",
	"Next Page Set Link" : "N=&Nao=100&Np=2?=p_PRODUCT_ID",

	"Direct Page Links" : [
		"N=&Nao=0&Np=2?=p_PRODUCT_ID",
		"N=&Nao=10&Np=2?=p_PRODUCT_ID",
		"N=&Nao=20&Np=2?=p_PRODUCT_ID",
		"N=&Nao=30&Np=2?=p_PRODUCT_ID",
		"N=&Nao=40&Np=2?=p_PRODUCT_ID",
		"N=&Nao=50&Np=2?=p_PRODUCT_ID",
		"N=&Nao=60&Np=2?=p_PRODUCT_ID",
		"N=&Nao=70&Np=2?=p_PRODUCT_ID",
		"N=&Nao=80&Np=2?=p_PRODUCT_ID",
		"N=&Nao=90&Np=2?=p_PRODUCT_ID"
	],
	"Direct Page Link Start Page" : "1",
},

*///
// Endeca Query Parser Class.
// This class wraps functionality to provide some level of "sane-ness" to the
// query-string syntax for posting requests to the Endeca server.
// The class takes some basic params, assumes defaults when necessary, and
// will generate a query string that can be sent to the Endeca server.
// It provides a "makeRequest()" function, which sends the request via Ajax
// and calls a callback function when complete.
// You can also use this class to re-query using strings received
// from a previous Endeca call (e.g. for paging, etc).
//
// IMPORTANT NOTE - This class does NOT (and should not) attempt to maintain
// state data about the page or the connection.  It's purpose is ONLY to
// generate a query and call a handler upon completion.  All other operations
// (e.g. "coremetrics", etc) should be handled by external objects.
//
// Use the EndecaCatalog and EndecaMeta classes to parse the response data.
//
// A very basic example, which creates a query for the word "lipstick", and
// uses the EndecaCatalog and EndecaMeta classes to parse the response into
// two global objects, and then calls "handleEndecaData()" when complete:
//
//  var eQuery = new EndecaQuery({
//		queryString: '',
//		searchTerm:  'lipstick',
//		filterProducts: true,
//		recsPerPage: 5,
//		callbackCompleted: function() {
//					g_eCatalog     = new EndecaCatalog({jsonResult: eQuery.jsonResult});
//					g_eCatalogMeta = new EndecaMeta({jsonResult: eQuery.jsonResult});
//					
//					handleEndecaData();
//			  	}		
//		});
//		
//	eQuery.makeRequest();


// Config params

// Port numbers of brand instances on the Endeca server:
var g_EndecaPort = {
	'MACUS': 16000,
	'MACCA': 16005,
	'MACNA': 16015
};
// Url for Endeca server.  'localhost' is relative to the apache server on the Endeca server.
var g_EndecaHost = 'localhost';
var g_EndecaUrl = '/enrpc/JSONControllerServlet.do';

// Brand/Endeca instance of Dimension id's we should ALWAYS include in "Ne" parms:
var g_EndecaNeIDs = {
	'MACUS': [8061, 8062, 8127, 8053, 8089, 8051, 8095, 8096, 8052, 8054 ],
	'MACCA': [8061, 8062, 8127, 8053, 8089, 8051, 8095, 8096, 8052, 8054 ],
	'MACNA': [8061, 8062, 8127, 8053, 8089, 8051, 8095, 8096, 8052, 8054,8196, 8197, 8198, 8199, 8200, 8201, 8202, 8203, 8204, 8205 ]
};

var g_EndecaTypeAheadDimIds = {
	'MACUS': [8097],
	'MACCA': [8097],
	'MACNA': [8097]
};

var g_EndecaTypeAheadDimKeys = {
	'MACUS': ['TypeAheadSearch'],
	'MACCA': ['TypeAheadSearch'],
	'MACNA': ['TypeAheadSearch']
};

var g_EndecaLogging = {
	'MACUS': {
		'dev': {
			'host': 'localhost',
			'port': 16004
		},
		'www': {
			'host': 'njlndca01',
			'port': 16004
		}
	},
	'MACCA': {
		'dev': {
			'host': 'localhost',
			'port': 16009
		},
		'www': {
			'host': 'njlndca01',
			'port': 16009
		}
	},
	'MACNA': {
		'dev': {
			'host': 'localhost',
			'port': 16019
		},
		'www': {
			'host': 'njlndca01',
			'port': 16019
		}
	}
};


// Session id for this query
// CAUTION - basing this on "ngglobal" cookie, which is actually "generic"
// but may not apply to all brands.  Also, in a way, this is "dirty" cuz
// it's not purely related to Endeca, but for now it gets us there.
// We COULD make a unique id that we stick in this session...
var __eq_session_id = 0;
var getEQSessionId = function() {
	if ( !__eq_session_id ) {
		var cval = generic.cookie('ngglobal');
		if ( cval ) {
			__eq_session_id = cval;
		} else {
			__eq_session_id = Math.floor(Math.random()*999999)+1;
		}
	}
	return __eq_session_id;
}


var EndecaQuery = Class.create({

	initialize: function(args) {
		this.queryString = '';
		this.callbackCompleted  = function(){};
		this.rawResult = '';
		this.brand = 'MACNA';
		this.server = ( location.hostname.indexOf('www') >= 0 ? 'www' : 'dev' );
		// The N record can be a number, or zero (for root), or a list (array)
		this.NRecordId = 0;
		this.NeRecordId = '';
		this.NaoRecordId = 0;
		this.rollupId = 'p_PRODUCT_ID';
		this.rollupProducts = true;
		this.rollupDetail = true;
		this.computePhrasings = true;
		this.searchTerm = '';
		this.searchMode = '';
		this.searchMatchMode = 'matchallpartial'; // default
		//this.searchMatchMode = 'matchany'; // default
		this.searchKey = '';
		this.didYouMean = 1;
		this.sortKey = '';
		this.sortAsc = true;
		this.rangeFilter = '';
		this.recordFilter = '';
		this.recsPerPage = 10;
		this.sessionId = getEQSessionId();
		this.enableLogging = true;
		
		// Use this to request one specific exact record, using "rec_id" property.
		// IMPORTANT: This will effectively override all other params!
		this.recordSpecId = '';
		
		this.searchDimensions = true;
		
		// This flag, if true, triggers an abbreviated (quick) search for type-ahead
		this.typeAheadSearch = false;
		
		// Used as Nf if rangeFilter is blank:
		this.filterShoppable = false;
		this.filterTRProducts = false;
		this.filterPromotional = false;
		this.filterDisplayable = false;
		this.filterSearchable = true; //true;
		this.filterInactiveSkus = true;
		
		// Used for Nr (record filter) if recordFilter is blank.
		// Generally you'll only search for one of these
		// (else leave off to search for all types)
		this.filterProducts = false;
		this.filterContent = false;
		this.filterLocale = true; //true;
		this.filterProProducts = true;
		this.additionalFilters = '';
		
		// Load up passed params
        Object.extend(this, args || {});
	},
	
	// Given a query string, parse it into our internal data.
	// When in doubt, use some rational default.
	parseQueryString: function(qs) {
		if ( qs ) {
			var qparms = $H(qs.toQueryParams());
			var val,arv;
			val = qparms.get('N');
			this.NRecordId = ( val == undefined ? 0 : val.split('+') );
			val = qparms.get('Ne');
			this.NeRecordId = ( val == undefined ? '' : val.split('+') );
			val = qparms.get('Nu');
			this.rollupId = ( val == undefined ? 'p_PRODUCT_ID' : val );
			val = qparms.get('Nao');
			this.NaoRecordId = ( val == undefined ? 0 : val );
			val = qparms.get('Np');
			this.rollupDetail = ( val == undefined ? 2 : val );
			val = qparms.get('Ntt');
			this.searchTerm = ( val == undefined ? '' : unescape(val) );
			val = qparms.get('Ntk');
			this.searchKey = ( val == undefined ? '' : val );
			val = qparms.get('Ntx');
			this.searchMode = ( val == undefined ? '' : val );
			val = qparms.get('Nty');
			this.didYouMean = ( val == undefined ? 0 : parseInt(val) ? 1 : 0 );
			val = qparms.get('Nf');
			this.rangeFilter = ( val == undefined ? '' : val );
			val = qparms.get('Nr');
			this.recordFilter = ( val == undefined ? '' : val );
			val = qparms.get('Ns');
			arv = ( val ? val.split('|') : [] );
			this.sortKey = ( val && arv[0] ? arv[0] : '' );
			this.sortAsc = ( arv[1] ? false : true );
			val = qparms.get('R');
			this.recordSpecId = ( val == undefined ? '' : val );
		}
	},

	// Build the query string to send to Endeca	
	buildQueryString: function(qs) {
		
		// Check for given query string
		if ( this.queryString && !qs ) {
			qs = this.queryString;
		}
		
		// Parse out query string if we have it.
		if ( qs ) {
			this.parseQueryString(qs);
		}
		
		// Clean up search term a little
		this.searchTerm = String(this.searchTerm).strip();
		
		// Build the array of parms we'll use for the new query string
		var arQS = [
			this.getM(),
			this.getL(),
			this.getN(),
			this.getNe(),
			this.getNao(),
			this.getNu(),
			this.getNp(),
			this.getNtt(),
			this.getNtk(),
			this.getNtx(),
			this.getNty(),
			this.getNf(),
			this.getNr(),
			this.getNs(),
			this.getNtpc(),
			this.getNtpr(),
			this.getD(),
			this.getDx(),
			this.getDi(),
			this.getR()
		];
		
		// Filter out empty parms
		arQS = arQS.findAll(function(el){return el.length > 0;});
		
		// Make the query string
		this.queryString = arQS.join('&');
		
		return this.queryString;
	},
	
	makeRequest: function(qs) {
		qs = this.buildQueryString(qs);
		var url = g_EndecaUrl + '?' + qs;
		new Ajax.Request ( url, {
	  		method: 'get',
	  		onComplete: this.onComplete.bind(this)
	  	});
	},
	
	onComplete: function(t) {
		this.rawResult = t.responseText;
		this.jsonResult = this.rawResult.evalJSON();
		this.callbackCompleted(this);
	},
	
	getM: function() {
		var mStr = '';
		
		// If we know the port, set it here
		var port = g_EndecaPort[this.brand];
		if ( port ) {
			mStr += 'host:' + g_EndecaHost + '|' + 'port:' + port;
		}
		
		// If recsPerPage is not the Endeca "default" value, set it here.
		if ( this.recsPerPage != 10 ) {
			if ( mStr.length > 0 ) mStr += '|';
			mStr += 'recs_per_page:' + this.recsPerPage;
		}
		
		if ( mStr.length > 0 ) {
			mStr = 'M=' + mStr;
		}
		
		return mStr;
	},
	
	getL: function() {
		var mStr = '';
		
		if ( this.enableLogging &&
			 g_EndecaLogging[this.brand] &&
			 g_EndecaLogging[this.brand][this.server] ) {
			var host = g_EndecaLogging[this.brand][this.server]['host'];
			var port = g_EndecaLogging[this.brand][this.server]['port'];
			
			mStr = 'L=SESSION_ID:'+this.sessionId+'|host:'+host+'|port:'+port;
		}
		
		return mStr;
	},
	
	getN: function() {
		// The N record can be a number, or zero (for root), or a list
		if ( this.recordSpecId ) return '';
		var val = this.NRecordId;
		if ( typeof this.NRecordId != "undefined" ) {
			val = $A(this.NRecordId).join('+');
		} else {
		    val = 0;
		}
		return 'N='+val;
		//return ( this.searchTerm.blank() ? 'N=0' : 'N=' );
	},
	
	// Ne - ID's of dimensions we want to return values for by default.
	// These ID's are generated by Endeca when dimensions are created, so they
	// will be unique per brand/endeca instance.
	getNe: function() {
		if ( this.recordSpecId ) return '';
		var ar = g_EndecaNeIDs[this.brand];
		if ( typeof this.NeRecordId != "undefined" ) {
			ar = ar.concat($A(this.NeRecordId));
		}
		var str = ( ar ? 'Ne='+ar.join('+') : '' );
		return str;
	},
	
	getNao: function() {
		// This is the page record id.
		if ( this.recordSpecId ) return '';
		var val = ( this.NaoRecordId ? this.NaoRecordId : 0 );
		return 'Nao='+val;
	},
	
	// Nu - rollup id to use.
	getNu: function() {
		if ( this.recordSpecId ) return '';
		var val = ( this.rollupId ? this.rollupId : 'p_PRODUCT_ID' );
		return ( this.rollupProducts ? 'Nu='+val : '' );
	},
	
	// Np - type of rollup.  1 = summary only, 2 = all records
	getNp: function() {
		if ( this.recordSpecId ) return '';
		var val = this.rollupDetail ? 2 : 1;
		return ( this.rollupProducts ? 'Np='+val : '' );
	},
	
	// Ntt - search term. This is the string to search for.
	getNtt: function() {
		if ( this.recordSpecId ) return '';
		//return ( this.searchTerm.blank() || this.typeAheadSearch ? '' : 'Ntt='+escape(this.searchTerm) );
		return ( this.searchTerm.blank() ? '' : 'Ntt='+encodeURIComponent(this.searchTerm) );
	},
	
	// Ntk - search key.  Usually "all", but can be one or more of pre-defined search keys
	// (e.g. "DESCRIPTION", "PRODUCT_NAME", etc).  Only return a value if we have a search term too.
	getNtk: function() {
		if ( this.recordSpecId ) return '';
		var str = '';
		
		//if ( !this.searchTerm.blank() && !this.typeAheadSearch ) {
		if ( !this.searchTerm.blank() ) {
			if ( this.searchKey.blank() ) {
				this.searchKey = 'all';
			}
			var skey = this.searchKey;
			
			if ( this.typeAheadSearch ) {
				var ar = g_EndecaTypeAheadDimKeys[this.brand];
				if ( ar ) {
					skey = ar.join('+');
				}
			}
			
			str = 'Ntk='+skey;
		}
		
		return str;
	},
	
	// Ntx - search mode.  This can get interesting, but for now we'll stick with all>partial.
	getNtx: function() {
		if ( this.recordSpecId ) return '';
		var str = '';

		//if ( !this.searchTerm.blank() && !this.typeAheadSearch ) {
		if ( !this.searchTerm.blank() ) {
			if ( this.searchMode.blank() ) {
				this.searchMode = 'mode+' + this.searchMatchMode;
			}
			str = 'Ntx='+this.searchMode;
		}

		return str;
	},
	
	// Nty - "did you mean" setting. 0=off, 1=on.  Only relevant when searching.
	getNty: function() {
		if ( this.recordSpecId ) return '';
		var str = '';

		if ( !this.searchTerm.blank() && this.didYouMean ) {
			str = 'Nty=1';
		}
		
		return str;
	},
	
	// Nf - range filter
	getNf: function() {
		if ( this.recordSpecId ) return '';
		var str = '';
		var arFilters = [];
		
		if ( this.rangeFilter.blank() ) {
			if ( this.filterShoppable ) arFilters.push('s_shoppable|GT+0');
			if ( this.filterTRProducts ) arFilters.push('p_TR_FLAG|LT+1');
			if ( this.filterPromotional ) arFilters.push('s_promotional|GT+0');
			if ( this.filterDisplayable ) arFilters.push('p_displayable|GT+0');
			if ( this.filterSearchable ) arFilters.push('s_searchable|GT+0');
			if ( arFilters.length > 0 )
				str = 'Nf='+arFilters.join('|');
		} else {
			// rangeFilter must be set to some string - just use it.
			str = 'Nf=' + this.rangeFilter;
		}
		
		return str;
	},
	
	// Nr - record filter
	getNr: function() {
		if ( this.recordSpecId ) return '';
		var str = '';
		var arFilters = [];
		
		if ( this.recordFilter.blank() ) {
			if ( this.filterProducts ) arFilters.push('rec_type:product');
			if ( this.filterContent  ) arFilters.push('rec_type:content');
			if ( this.filterLocale  ) arFilters.push('locale:' + generic.cookie('LOCALE'));
			if ( this.filterInactiveSkus  ) arFilters.push('NOT(s_INVENTORY_STATUS:5)');
			if ( this.filterProProducts && !global.ispro ) arFilters.push('NOT(s_PRO_FLAG:1)');
			if ( this.additionalFilters ) arFilters.push(this.additionalFilters);
			
			if ( arFilters.length > 0 )
				str = 'Nr=AND(' + arFilters.join(',') + ')';
		} else {
			str = 'Nr=' + this.recordFilter;
		}
		
		return str;
	},
	
	getNs: function() {
		if ( this.recordSpecId ) return '';
		var str = '';
		if ( this.sortKey ) {
			str = 'Ns=' + this.sortKey;
			if ( !this.sortAsc ) {
				str += '|1';
			}
		}
		return str;
	},
	
	getNtpc: function() {
		if ( this.recordSpecId ) return '';
		var str = ( this.computePhrasings && !this.searchTerm.blank() ? 'Ntpc=1' : '' );
		return str;
	},

	getNtpr: function() {
		if ( this.recordSpecId ) return '';
		var str = ( this.computePhrasings && !this.searchTerm.blank() ? 'Ntpr=1' : '' );
		return str;
	},
	
	// D and Dx are dimension search analogues to Ntt and Ntx.
	getD: function() {
		if ( this.recordSpecId ) return '';
		var str = '';
		
		if ( this.searchDimensions && !this.searchTerm.blank() ) {
			str = 'D='+encodeURIComponent(this.searchTerm);
		}
		
		return str;
	},
	
	getDx: function() {
		if ( this.recordSpecId ) return '';
		var str = '';

		if ( this.searchDimensions && !this.searchTerm.blank() ) {
			if ( this.searchMode.blank() ) {
				this.searchMode = 'mode+' + this.searchMatchMode;
			}
			str = 'Dx='+this.searchMode;
		}

		return str;
	},
	
	getDi: function() {
		if ( this.recordSpecId ) return '';
		var str = '';
		
		// Don't bother unless this is a typeahead search
		if ( this.searchDimensions && !this.searchTerm.blank() && this.typeAheadSearch ) {
			var ar = g_EndecaTypeAheadDimIds[this.brand];
			if ( ar ) {
				str = 'Di='+ar.join('+');
			}
		}
		
		return str;
	},
	
	getR: function() {
		var str = '';
		
		if ( this.recordSpecId ) {
			str = 'R=' + this.recordSpecId;
		}
		
		return str;
	}
	
});

/*
thoughts on data:

should have a catprodsku.js file that has classes for:
	ECategory
	EProduct
	ESku
	ECategoryList
	EProductList
	ESkuList
	ECatalog
	
Catalog is entry point.  Maintains *List objects.
Can parse current prodcat json or endeca.

ECatalog.addRecord()
 - takes one record of "Properties" hash in result set
 - Calls ECategoryList.addRecord()
 	- Looks up existing ECategory object or creates new one.
 	- Pulls out all "c_" fields
 	- Calls EProduct.addRecord()
 		- Pulls out all "p_" fields
 		- Calls ESku.addRecord()
 			- Pulls out all "s_"fields

hmm... rollup on p_path instead of p_PRODUCT_ID?

Data Hash Format:

	AggrRecords: [
		{	-- new product
			Records: [
				{
					Properties: {
						c_*
						p_*
						s_*
					}
				}
				{
					Properties: {
						c_*
						p_*
						s_*
					}
				}
			]
		}
		{	-- new product
			Records: [
				{
					Properties: {
						c_*
						p_*
						s_*
					}
				}
				{
					Properties: {
						c_*
						p_*
						s_*
					}
				}
			]
		}
	]
	
resultData = $H(results);
resultData['AggrRecords'].each(function(prodHash) {
	prodHash['Records'].each(function(recHash) {
		var propHash = recHash['Properties'];
		var catProps = propHash.findAll(function(el){return el.key
	});
});

*/
// Endeca
// Helper functions for CoreMetrics
// Called when we first start processing a response from the server.
// Can/should be used to init data for this page view.
var endeca_tags={};

var ecatCmBeginProcessing = function() {

   endeca_tags['pcount'] = 0;
   endeca_tags['ccount']=0;
   endeca_tags['term']='';
   endeca_tags['filter_list']=  ''; 
   endeca_tags['current_page']=0;
   endeca_tags['page_count']=0;
}

// Set current search term for CM.
// "sterm" is the search term (string).
var ecatCmSetSearchTerm = function(sterm) {
       //console.log("TERM ",sterm);
   endeca_tags['term'] = sterm;
 
}

// Add another filter name to the CM list of filters.
// Will be called once for each filter used in the request.
// "fname" is the value of the filter.
var ecatCmAddFilter = function(dim,fname) {
      //console.log("Filter list ",fname,dim);
      //console.log("Existing Filters ", endeca_tags['filter_list']);
   var sf = {
      "Skin Type" : "Typ",
      "Skin Tone" : "Ton"
  };
  if (typeof sf[dim] != "undefined"){
    dim = sf[dim];
       //console.log("SF ran",dim);
  }else{
    var filter_words = dim.split(' ');
         //console.log("filter words ", filter_words); 
       if (filter_words.size() > 1){
            dim = filter_words[0].substr(0,3);
            for (i=1; i<=filter_words.size() - 1;i++){
              dim = dim + filter_words[i].substr(0,1);
            } 
       }else{
          dim = dim.substr(0,3);
       }
  } 
   if (fname){
      if (endeca_tags['filter_list'].length > 1 ){
        endeca_tags['filter_list'] = endeca_tags['filter_list'] +' > '+ dim + ':'+ fname;
       }else{
       endeca_tags['filter_list'] = dim + ':' +fname;
       }
   } 
}

// Called when we know how many results we have.
// Indicates total number of products and content items found.
var ecatCmResultCount = function(countProducts,countContent) {
   endeca_tags['pcount'] = countProducts;
   endeca_tags['ccount'] = countContent;
  console.log("ccount ", countContent);
  console.log("pcount ", countProducts);
}

// Called to indicate which page we're on and how many total pages.
var ecatCmPageNumber = function(currentPage,totalPages) {
   endeca_tags['current_page']=currentPage;
   endeca_tags['page_count']=totalPages;

}

// Called when we're done processing results.
// Can/should be used to send the actual page view tag
// using the data provided by the above calls.
var ecatCmFinishProcessing = function() {
  var page_count = endeca_tags['current_page'] || '1';
  var PAGE_ID = 'Search Results ' + page_count;
  //if (page_count < 1 && endeca_tags['ccount'] == 0){
   //   PAGE_ID = PAGE_ID + " BestSellers";
  //}
  if (endeca_tags['filter_list'].length > 0){
      PAGE_ID = 'Search Results Filtered ' + page_count;
  }
  var CATID = '2200';
  var KEYWORDS = endeca_tags['term'];

   if (endeca_tags['ccount'] > 0 && endeca_tags['pcount'] == 0){
           KEYWORDS = '*' + KEYWORDS;
   }

  var RESULTS = endeca_tags['ccount'] + endeca_tags['pcount'] || '0';
  
  cmCreatePageviewTag(PAGE_ID, KEYWORDS,CATID,RESULTS,endeca_tags['filter_list']);
   

}

var ecatCmContentClick  = function() {
  cmCreatePageElementTag("CONTENT", "SEARCH DROPDOWN");
}
var ecatCmProductClick = function() {
 cmCreatePageElementTag("PRODUCTS", "SEARCH DROPDOWN");
} 
var ecatCmSeeAll = function() {
  cmCreatePageElementTag("SEE ALL","SEARCH DROPDOWN");
}

// end Endeca
/**
 * 3rd party code for Motion Point
 * ES language toggling
 */
var MP = {
<!-- mp_trans_disable_start --> 
    Version: '1.0.22',
    Domains: {'es':'espanol.maccosmetics.com'}, 
    SrcLang: 'en',
<!-- mp_trans_disable_end -->
    UrlLang: 'mp_js_current_lang',
    SrcUrl: unescape('mp_js_orgin_url'),

<!-- mp_trans_disable_start -->

    init: function(){
        if (MP.UrlLang.indexOf('p_js_')==1) {
            MP.SrcUrl=window.top.document.location.href;
            MP.UrlLang=MP.SrcLang;
        }
    },
    getCookie: function(name){
        var start=document.cookie.indexOf(name+'=');
        if(start < 0) return null;
        start=start+name.length+1;
        var end=document.cookie.indexOf(';', start);
        if(end < 0) end=document.cookie.length;
        while (document.cookie.charAt(start)==' '){ start++; }
        return unescape(document.cookie.substring(start,end));
    },
    setCookie: function(name,value,path,domain){
        var cookie=name+'='+escape(value);
        if(path)cookie+='; path='+path;
        if(domain)cookie+='; domain='+domain;
        var now=new Date();
        now.setTime(now.getTime()+1000*60*60*24*365);
        cookie+='; expires='+now.toUTCString();
        document.cookie=cookie;
    },
    switchLanguage: function(lang){
        if(lang!=MP.SrcLang){
            var script=document.createElement('SCRIPT');
            script.src=location.protocol+'//'+MP.Domains[lang]+'/'+MP.SrcLang+lang+'/?1023749632;'+encodeURIComponent(MP.SrcUrl);
            document.body.appendChild(script);
        } else if(lang==MP.SrcLang && MP.UrlLang!=MP.SrcLang){
            var script=document.createElement('SCRIPT');
            script.src=location.protocol+'//'+MP.Domains[MP.UrlLang]+'/'+MP.SrcLang+MP.UrlLang+'/?1023749634;'+encodeURIComponent(location.href);
            document.body.appendChild(script);
        }
        return false;
    },
    switchToLang: function(url) {
        window.top.location.href=url; 
    },
    
    // Mac method:
    // Set up behavior for inline links on site that should switch locale states
    // Example: macpro renewal page
    initRedirectLinks: function() {
        var enLinkNodes = $$("a.locale-redirect-to-en");
        var esLinkNodes = $$("a.locale-redirect-to-es");
        var currentUrl = window.location.href.split(".com");
        var currentDomain = currentUrl[0];

        // if language=ES url param found, set to ES
        var domain = "maccosmetics.com";
        var redirectUrl = "maccosmetics.com";
        if (currentDomain.indexOf("stage") > -1) {
            redirectUrl = "maccosmetics.na.stage.ncsa.perlgem.esteeonline.com";
            domain = "esteeonline.com";
        } else if ((currentDomain.indexOf("dev") > -1) || (currentDomain.indexOf("eng") > -1) ) {
            redirectUrl = "maccosmetics.na.dev.ncsa.perlgem.esteeonline.com";
            domain = "esteeonline.com";
        }
           
        var onClick = function(event, node, selectedLang) {
            event.preventDefault();
            var href = node.href.split(".com")[1] || "";
            if (selectedLang === "es") {
                // set to spanish
                MP.setCookie('mptrans', 'ES', '/', domain);
                redirectUrl = "espanol." + redirectUrl;
            
            // set to EN
            } else {
                MP.setCookie('mptrans', '', '/', domain);
            }
            redirectUrl = "http://" + redirectUrl + href;
            //console.log("setting to "+selectedLang+" domain = "+domain);
            //console.log("redirecting to "+redirectUrl);
            window.location = redirectUrl;            
        }
        enLinkNodes.each( function(node) {
            node.observe("click", function(event) {
                onClick(event, node, "en");
            });
        });
        esLinkNodes.each( function(node) {
            node.observe("click", function(event) {
                onClick(event, node, "es");
            });
        });
    }
    
<!-- mp_trans_disable_end -->   
};


