var lensHtmlUtils = {

    showTabsOrNot: function showTabsOrNot() {
        const logHdr = 'showTabsOrNot: ';
        log(logHdr + 'enter');
        log(logHdr + 'viewWidth=' + viewWidth);

        const showTabs = (viewWidth > 375);
        // if the view is wide enough to show tabs in the lens cards

        log(logHdr + 'showTabs=' + showTabs);
        log(logHdr + 'leave');

        return showTabs;
    },

    _getHtmlForLensCardWithTabsAsync: async function _getHtmlForLensCardWithTabs(lens, expanded, explicitWidthStr) {
        // this function returns HTML for a lens card with 4 tabs:
        //  When, How, Examples, Video
        //
        // expanded: true if we should create the card in expanded mode
        // explicitWidthStr: undefined or empty string: do nothing
        //  otherwise, this string is a CSS width value for the lens card div
        //  for example:
        //      600px
        //      90%

        const logHdr = '_getHtmlForLensCardWithTabsAsync: ';
        log(logHdr + 'enter');
        log(logHdr + 'lens name=' + lens.LensName);
        log(logHdr + 'expanded=' + expanded);
        log(logHdr + 'explicitWidthStr=' + explicitWidthStr);

        var widthStr = explicitWidthStr;

        if (typeof widthStr === "undefined") {
            widthStr = '';
        }

        if (widthStr === null)
            widthStr = '';

        // lens: a lens DTO
        // expanded: if true, show the card in expanded mode, with tabs revealed
        //  otherwise, show card in collpased mode, with tabs hidden.

        // NOTE: we could use
        //  '<h5 class="card-title">' + lens.LensName + '</h5>'
        // for a large card title at the top of the card

        var lensId = lens.LensId;

        log(logHdr + 'lensId=' + lensId);

        const bookmarked = bookmarkStorageUtils.isLensBookmarked(lensId);

        log(logHdr + 'bookmarked=' + bookmarked);

        const descrHtml = lens.DescriptionHtml;
        log(logHdr + 'descrHtml=' + descrHtml);

        var styleStr = '';
        if (widthStr.length > 0) {
            styleStr = ' style="width: ' + widthStr + '" ';
        }

        var aWhen = '';
        var aHow = '';
        var aExamples = '';
        var aVideo = '';
        // clear

        var divWhen = '';
        var divHow = '';
        var divExamples = '';
        var divVideo = '';
        // clear

        const selTab = selectedTabUtils.getSelectedTab(lensId);
        // get the name of the selected tab, or empty string if none selected

        log(logHdr + 'selTab=' + selTab);

        switch (selTab) {
            case '':
                aWhen = 'active';
                divWhen = 'active show';
                break;

            case 'when':
                aWhen = 'active';
                divWhen = 'active show';
                break;

            case 'how':
                aHow = 'active';
                divHow = 'active show';
                break;

            case 'examples':
                aExamples = 'active';
                divExamples = 'active show';
                break;

            case 'video':
                aVideo = 'active';
                divVideo = 'active show';
                break;
        }

        const cardId = elemIdUtils.getElemId(lensId, elemIdUtils.CARD_PREFIX, undefined);
        // get the id of the card div
        // this div itself doesn't show/hide, so the third parameter is undefined

        var lensHtml = '<div id="' + cardId + '" class="card"' + styleStr + '>';

        const cardHdrId = elemIdUtils.getElemId(lensId, elemIdUtils.CARD_HEADER_PREFIX, undefined);

        lensHtml += '<div id="' + cardHdrId + '" class="card-hdr">';

        const nameSpanId = elemIdUtils.getElemId(lensId, elemIdUtils.SPAN_LENS_NAME_PREFIX, undefined);

        lensHtml += '<span id="' + nameSpanId + '" class="card-hdr-lens-name">' + lens.LensName + '</span>';

        const btnId = elemIdUtils.getElemId(lensId, elemIdUtils.BUTTON_CLOSE_CARD_HEADER_PREFIX, expanded);

        lensHtml += '<button id="' + btnId + '" class="btn card-hdr-close-btn float-right" type="button" aria-label="Close"><span class="card-hdr-close-x" aria-hidden="true">&times;</span></button>';

        const showBk = bookmarkUIUtils.showBookmarks();
        // should we show bookmark icons?

        log('getHtmlForLensCard: showBk=' + showBk);

        if (showBk) {
            // if we should show bookmarks

            var wImgPath = '';
            var bkClass = '';

            if (viewWide) {
                // if the user is using a wider screen, we show smaller bookmarks

                bkClass = 'bk-sm';

                if (bookmarked) {
                    // if the lens is bookmarked

                    wImgPath = urlUtils.getUrl('/images/bookmarkfilled-sm.png');
                }
                else {
                    // if the lens is not bookmarked

                    wImgPath = urlUtils.getUrl('/images/bookmarkempty-sm.png');
                }
            }
            else {
                // if the user is on a smaller screen, we show larger bookmarks

                bkClass = 'bk-lg';

                if (bookmarked) {
                    // if the lens is bookmarked

                    wImgPath = urlUtils.getUrl('/images/bookmarkfilled.png');
                }
                else {
                    // if the lens is not bookmarked

                    wImgPath = urlUtils.getUrl('/images/bookmarkempty.png');
                }
            }

            const imgBkId = elemIdUtils.getElemId(lensId, elemIdUtils.IMAGE_BOOKMARK_PREFIX, expanded);

            lensHtml += '<img id="' + imgBkId + '" class="empty-bookmark ' + bkClass + '" src="' + wImgPath + '" />';
            // add the bookmark image to the lens markup
        }

        lensHtml += '</div>';

        lensHtml += '<div class="card-body">';

        const spanId = elemIdUtils.getElemId(lensId, elemIdUtils.SPAN_LENS_NAME_PREFIX, undefined);

        const pDescr = descrHtml.replace('<span ', '<span id="' + spanId + '" ');
        // store an HTML element id in the span element in the description

        const cardDescrId = elemIdUtils.getElemId(lensId, elemIdUtils.CARD_DESCR_PREFIX, undefined);

        lensHtml += '<p id="' + cardDescrId + '" class="card-text">' + pDescr;

        lensHtml += '&nbsp;&nbsp;&nbsp;';

        var moreStyle = '';
        var lessStyle = '';

        if (expanded) {
            // if the caller wants the card created in expanded mode

            moreStyle = ' style="display: none;"';
            // mode is already more, so hide the more link
            lessStyle = '';
        }
        else {
            // if the caller wants the card created in collapsed mode

            moreStyle = '';
            lessStyle = ' style="display: none;"';
            // mode is already less, so hide the less link
        }

        const spanMoreId = elemIdUtils.getElemId(lensId, elemIdUtils.SPAN_MORE_PREFIX, undefined);
        const spanLessId = elemIdUtils.getElemId(lensId, elemIdUtils.SPAN_LESS_PREFIX, undefined);

        // the "more..." link which allows the user to expand the card

        // NOTE: s-more-tabs is the class for "more" links in cards with tabs
        // NOTE: s-less-tabs is the class for "less" links in cards with tabs

        lensHtml += '<span id="' + spanMoreId + '" class="s-more-tabs"' + moreStyle + ' > ';
        lensHtml += 'more...';
        lensHtml += '</span>';

        // the "less..." link which allows the user to collapse the card
        lensHtml += '<span id="' + spanLessId + '" class="s-less-tabs"' + lessStyle + ' > ';
        lensHtml += 'less...';
        lensHtml += '</span>';

        lensHtml += '</p>';

        var displayStyle = 'display: none;';

        if (expanded) {
            // if the caller wants the tabs displayed

            displayStyle = 'display: block;';
        }

        const collapseId = elemIdUtils.getElemId(lensId, elemIdUtils.COLLAPSE_TABS_PREFIX, undefined);
        // while the tabs do show/hide, there is a max of one set of tabs per card
        // so the expanded flag is undefined

        lensHtml += '<div id="' + collapseId + '" class="collapse-tabs" style="' + displayStyle + '">';

        const cardTabId = elemIdUtils.getElemId(lensId, elemIdUtils.LIST_PREFIX, expanded);

        // this code uses Bootstrap and <ul>/<li> elements to display the tabs
        lensHtml += '<ul class="nav nav-tabs" id="' + cardTabId + '" role="tablist">';

        const liWhenId = elemIdUtils.getElemId(lensId, elemIdUtils.LIST_ITEM_WHEN_TAB_PREFIX, expanded);
        const liHowId = elemIdUtils.getElemId(lensId, elemIdUtils.LIST_ITEM_HOW_TAB_PREFIX, expanded);
        const liExId = elemIdUtils.getElemId(lensId, elemIdUtils.LIST_ITEM_EXAMPLES_TAB_PREFIX, expanded);
        const liVideoId = elemIdUtils.getElemId(lensId, elemIdUtils.LIST_ITEM_VIDEO_TAB_PREFIX, expanded);

        const whenId = elemIdUtils.getElemId(lensId, elemIdUtils.TAB_PANE_WHEN_PREFIX, expanded);
        const howId = elemIdUtils.getElemId(lensId, elemIdUtils.TAB_PANE_HOW_PREFIX, expanded);
        const examplesId = elemIdUtils.getElemId(lensId, elemIdUtils.TAB_PANE_EXAMPLES_PREFIX, expanded);


        const videoId = elemIdUtils.getElemId(lensId, elemIdUtils.TAB_PANE_VIDEO_PREFIX, undefined);
        // undefined => there is only one of these elements for each lens

        // create unique ids for the nav-link elements

        lensHtml += '<li id="' + liWhenId + '" class="nav-item card-tab-caption when-tab">';
        lensHtml += '<a class="nav-link ' + aWhen + '" id="' + whenId + '-tab" data-toggle="tab" href="#' + whenId + '" role="tab" aria-controls="' + whenId + '" aria-selected="true">When</a>';
        lensHtml += '</li>';

        lensHtml += '<li id="' + liHowId + '" class="nav-item card-tab-caption how-tab">';
        lensHtml += '<a class="nav-link ' + aHow + '" id="' + howId + '-tab" data-toggle="tab" href="#' + howId + '" role="tab" aria-controls="' + howId + '" aria-selected="false">How</a>';
        lensHtml += '</li>';

        lensHtml += '<li id="' + liExId + '" class="nav-item card-tab-caption examples-tab">';
        lensHtml += '<a class="nav-link ' + aExamples + '" id="' + examplesId + '-tab" data-toggle="tab" href="#' + examplesId + '" role="tab" aria-controls="' + examplesId + '" aria-selected="false">Examples</a>';
        lensHtml += '</li>';

        lensHtml += '<li id="' + liVideoId + '" class="nav-item card-tab-caption video-tab">';

        lensHtml += '<a class="nav-link ' + aVideo + '" id="' + videoId + '-tab" data-toggle="tab" href="#' + videoId + '" role="tab" aria-controls="' + videoId + '" aria-selected="false">Video</a>';

        lensHtml += '</li>';

        lensHtml += '</ul>';

        const tcId = elemIdUtils.getElemId(lensId, elemIdUtils.CARD_TAB_CONTENT_PREFIX, undefined);

        lensHtml += '<div class="tab-content" id="' + tcId + '">';

        lensHtml += '<div class="tab-pane fade card-text-when ' + divWhen + '" id="' + whenId + '" role="tabpanel" aria-labelledby="when-tab">' + lens.WhenToUse + '</div>';

        lensHtml += '<div class="tab-pane fade card-text-how ' + divHow + '" id="' + howId + '" role="tabpanel" aria-labelledby="how-tab">' + lens.HowToUse + '</div>';

        const lensExamples = lensExampleUtils.getExamplesForLens(lensId);

        const exCount = lensExamples.length;

        log(logHdr + 'exCount=' + exCount);

        var examplesHtml = '';

        for (var zIndex = 0; zIndex < exCount; zIndex++) {
            // once for each example

            var lensExample = lensExamples[zIndex];

            var exTitle = lensExample.Name;
            var exText = lensExample.Text;

            var exTitleHtml = '<div class="example-title">' + exTitle + '</div>'
            var exTextHtml = '<div class="example-text">' + exText + '</div>'

            examplesHtml += exTitleHtml;
            examplesHtml += exTextHtml;
            // append example title and text to the HTML
        }

        lensHtml += '<div class="tab-pane fade lens-examples ' + divExamples + '" id="' + examplesId + '" role="tabpanel" aria-labelledby="examples-tab">' + examplesHtml + '</div>';

        lensHtml += '<div class="tab-pane fade ' + divVideo + '" id="' + videoId + '" role="tabpanel" aria-labelledby="video-tab">';
        // this is where we append the video element
        //  TAB_PANE_VIDEO_PREFIX

        if (aVideo.length > 0) {
            // if the Video tab will be selected, we need to add the video element now

            const videoType = videoUtils.getVideoType(lensId);
            // what is the video type for this lens?

            log(logHdr + 'videoType=' + videoType);
            log(logHdr + 'we need to add the video element');

            const videoElemSize = videoUtils.getVideoElemSize(viewWidth, viewHeight);
            // get the desired size of the video element given the view dimensions

            const videoElemWidth = videoElemSize.width;
            const videoElemHeight = videoElemSize.height;

            log(logHdr + 'viewWidth=' + viewWidth);
            log(logHdr + 'viewHeight=' + viewHeight);
            log(logHdr + 'video element size is ' + videoElemWidth + ' x ' + videoElemHeight);

            var videoHtml = '';

            if (videoType === videoUtils.VIDEO_TYPE_BLOB) {
                // if the video for the lens is a video element

                const blubUrl = videoUtils.getVideoUrl(lensId);
                // get the URL of the video blob

                videoHTML = videoUtils.getVideoHtml_video(lensId, videoElemWidth, videoElemHeight, blubUrl);
                // get the html for the video element

                log(logHdr + 'computed the HTML for the video element');
            }
            else {
                // if the video for the lens is an iFrame

                const iFrameSourceUrl = videoUtils.getIFrameUrl(lensId);
                // get the URL for the iFrame source

                log(logHdr + 'iFrameSourceUrl=' + iFrameSourceUrl);

                videoHTML = videoUtils.getVideoHtml_iFrame(lensId, videoElemWidth, videoElemHeight, iFrameSourceUrl);
                // get the html for the iFrame element

                log(logHdr + 'computed the HTML for the iFrame video element');
            }

            log('videoHTML=' + videoHTML);

            lensHtml += videoHTML;
            // add it to the markup

            // NOTE: code elsewhere will have to subscribe to video events for this video element
        }

        lensHtml += '</div> ';

        lensHtml += '</div>';
        // add markup for the tabs

        lensHtml += '</div>'; // card-body
        lensHtml += '</div>'; // card-hdr
        lensHtml += '</div>'; // card

        log(logHdr + ' leave');

        return lensHtml;
    },

    _getHtmlForLensCardNoTabs: function _getHtmlForLensCardNoTabs(lens, videoBtn, explicitWidthStr) {
        // this function returns HTML for a lens card without tabs.
        // instead of tabs, the HTML returned by this function contains 4 sections:
        // When, How, Examples, Video
        // separated by section headers, which are simply text captions.
        //
        // lens: a lens DTO
        // videoBtn: if true, add a button the user can click to load the video.
        //           if false, add the video element
        // explicitWidthStr: undefined or empty string: do nothing
        //  otherwise, this string is a CSS width value for the lens card div
        //  for example:
        //      600px
        //      90%

        // NOTE: as of 2/27/20 we hard-code to not show the Load Video button
        // All videos are loaded.
        // We plan on moving to all streaming (Vimeo) videos soon.

        const logHeader = '_getHtmlForLensCardNoTabs: ';
        log(logHeader + 'enter');
        log(logHeader + 'lens name=' + lens.LensName);
        log(logHeader + 'videoBtn=' + videoBtn + ' (IGNORED for now)');
        log(logHeader + 'explicitWidthStr=' + explicitWidthStr);

        var widthStr = explicitWidthStr;

        if (typeof widthStr === "undefined") {
            widthStr = '';
        }

        if (widthStr === null)
            widthStr = '';

        var lensId = lens.LensId;

        log(logHeader + 'lensId=' + lensId);

        const bookmarked = bookmarkStorageUtils.isLensBookmarked(lensId);

        log(logHeader + 'bookmarked=' + bookmarked);

        const descrHtml = lens.DescriptionHtml;
        log(logHeader + 'descrHtml=' + descrHtml);

        var styleStr = '';
        if (widthStr.length > 0) {
            styleStr = ' style="width: ' + widthStr + '" ';
        }

        const cardId = elemIdUtils.getElemId(lensId, elemIdUtils.CARD_PREFIX, undefined);
        // get an id for the card
        // don't pass the expanded flag
        // the card has expandable contents but the card div itself is not expandable

        var lensHtml = '<div id="' + cardId + '" class="card"' + styleStr + '>';

        const cardHdrId = elemIdUtils.getElemId(lensId, elemIdUtils.CARD_HEADER_PREFIX, undefined);

        lensHtml += '<div id="' + cardHdrId + '" class="card-hdr">';

        const nameSpanId = elemIdUtils.getElemId(lensId, elemIdUtils.SPAN_LENS_NAME_PREFIX, undefined);

        lensHtml += '<span id="' + nameSpanId + '" class="card-hdr-lens-name">' + lens.LensName + '</span>';

        const btnId = elemIdUtils.getElemId(lensId, elemIdUtils.BUTTON_CLOSE_CARD_HEADER_PREFIX, undefined);

        lensHtml += '<button id="' + btnId + '" class="btn card-hdr-close-btn float-right" type="button" aria-label="Close"><span class="card-hdr-close-x" aria-hidden="true">&times;</span></button>';

        const showBk = bookmarkUIUtils.showBookmarks();
        // should we show bookmark icons?

        log('getHtmlForLensCard: showBk=' + showBk);

        if (showBk) {
            // if we should show bookmarks

            var wImgPath = '';
            var bkClass = '';

            if (viewWide) {
                // if the user is using a wider screen, we show smaller bookmarks

                bkClass = 'bk-sm';

                if (bookmarked) {
                    // if the lens is bookmarked

                    wImgPath = urlUtils.getUrl('/images/bookmarkfilled-sm.png');
                }
                else {
                    // if the lens is not bookmarked

                    wImgPath = urlUtils.getUrl('/images/bookmarkempty-sm.png');
                }
            }
            else {
                // if the user is on a smaller screen, we show larger bookmarks

                bkClass = 'bk-lg';

                if (bookmarked) {
                    // if the lens is bookmarked

                    wImgPath = urlUtils.getUrl('/images/bookmarkfilled.png');
                }
                else {
                    // if the lens is not bookmarked

                    wImgPath = urlUtils.getUrl('/images/bookmarkempty.png');
                }
            }

            const imgId = elemIdUtils.getElemId(lensId, elemIdUtils.IMAGE_BOOKMARK_PREFIX, undefined);

            lensHtml += '<img id="' + imgId + '" class="empty-bookmark ' + bkClass + '" src="' + wImgPath + '" />';
            // add the bookmark image to the lens markup
        }

        lensHtml += '</div>'; // #cardHdr

        lensHtml += '<div class="card-body">';

        const lensNameSpanId = elemIdUtils.getElemId(lensId, elemIdUtils.SPAN_LENS_NAME_PREFIX, undefined);

        const pDescr = descrHtml.replace('<span ', '<span id="' + lensNameSpanId + '" ');
        // store an HTML element id in the span element in the description

        const cardDescrId = elemIdUtils.getElemId(lensId, elemIdUtils.CARD_DESCR_PREFIX, undefined);

        lensHtml += '<p id="' + cardDescrId + '" class="card-text">' + pDescr + '</p>';

        // NOTE: at this point in the code, we create two sets of content: collapsed and expanded

        var expHtml = '';
        // the HTML for the expanded contents

        if (true) {
            // create the code for the expanded content

            const eContentId = elemIdUtils.getElemId(lensId, elemIdUtils.CARD_TEXT_PREFIX, true);

            expHtml += '<div class="" id="' + eContentId + '">';

            expHtml += '<div class="section-caption">When to Use</div>';

            const eWhenId = elemIdUtils.getElemId(lensId, elemIdUtils.WHEN_CONTENT_PREFIX, true);

            expHtml += '<div class="section-content" id="' + eWhenId + '">' + lens.WhenToUse + '</div>';


            expHtml += '<div class="section-caption">How to Use</div>';

            const eHowId = elemIdUtils.getElemId(lensId, elemIdUtils.HOW_CONTENT_PREFIX, true);

            expHtml += '<div class="section-content" id="' + eHowId + '">' + lens.HowToUse + '</div>';

            expHtml += '<div class="section-caption">Examples</div>';

            const lensExamples = lensExampleUtils.getExamplesForLens(lensId);

            const exCount = lensExamples.length;

            log(logHeader + 'exCount=' + exCount);

            var examplesHtml = '';

            for (var zIndex = 0; zIndex < exCount; zIndex++) {
                // once for each example

                var lensExample = lensExamples[zIndex];

                var exTitle = lensExample.Name;
                var exText = lensExample.Text;

                var exTitleHtml = '<div class="example-title">' + exTitle + '</div>';
                var exTextHtml = '<div class="example-text">' + exText + '</div>';

                examplesHtml += exTitleHtml;
                examplesHtml += exTextHtml;
                // append example title and text to the HTML
            }

            const eExId = elemIdUtils.getElemId(lensId, elemIdUtils.EXAMPLES_CONTENT_PREFIX, true);

            expHtml += '<div class="section-content" id="' + eExId + '">' + examplesHtml + '</div>';

            expHtml += '<div  class="section-caption">Video</div>';

            const eVideoId = elemIdUtils.getElemId(lensId, elemIdUtils.VIDEO_CONTENT_PREFIX, true);

            expHtml += '<div class="" id="' + eVideoId + '">';

            // NOTE: for now, we don't show the Load Video button
            //if (videoBtn) {
            //    log(logHeader + 'add the "Load Video" button');

            //    const eBtnLoadVideoId = elemIdUtils.getElemId(lensId, elemIdUtils.BUTTON_LOAD_VIDEO_PREFIX, undefined);

            //    log(logHeader + 'eBtnLoadVideoId=' + eBtnLoadVideoId);

            //    const buttonHTML = '<button id="' + eBtnLoadVideoId + '" type="button" class="btn btn-primary load-video">Load Video</button>';

            //    expHtml += buttonHTML;
            //}

            log(logHeader + 'add the video markup to the HTML');

            var videoElemSize = lensUIUtils._getVideoElemSize(viewWidth, viewHeight);
            // get the desired size of the video element given the view dimensions

            var videoElemWidth = videoElemSize.width;
            var videoElemHeight = videoElemSize.height;

            const videoType = videoUtils.getVideoType(lensId);
            // what type of video will we create?

            log(logHeader + 'videoType=' + videoType);

            var videoHtml = '';
            var videoUrl = '';
            var videoElemId;

            videoElemId = elemIdUtils.getVideoElemId(lensId);
            // get the id of the video element

            log(logHeader + 'videoElemId=' + videoElemId);

            if (videoType === videoUtils.VIDEO_TYPE_BLOB) {
                // if blob video

                videoUrl = videoUtils.getVideoUrl(lensId);
                // get the URL of the blob

                videoHTML = videoUtils.getVideoHtml_video(lensId, videoElemWidth, videoElemHeight, videoUrl);
                // get html for the video
            }
            else {
                // if Vimeo video

                videoUrl = videoUtils.getIFrameUrl(lensId);
                // get the URL of the iFrame source

                videoHTML = videoUtils.getVideoHtml_iFrame(lensId, videoElemWidth, videoElemHeight, videoUrl);
                // get html for the Vimeo video in an iFrame
            }

            expHtml += videoHTML;
            // add the video HTML to the markup

            expHtml += '</div> '; // #videoContent

            const spanLessId = elemIdUtils.getElemId(lensId, elemIdUtils.SPAN_LESS_PREFIX, true);

            // the "less..." link which allows the user to collapse the card

            // NOTE: s-less is the class for "less" links in cards without tabs

            expHtml += '<span id="' + spanLessId + '" class="s-less"> ';
            expHtml += 'less...';
            expHtml += '</span>';

            expHtml += '</div>';  // #cardContent
        }

        const expElemId = elemIdUtils.getElemId(lensId, elemIdUtils.CARD_EXPANDED_CONTENT_PREFIX, undefined);

        const expanded = noTabsCardExpStorageUtils.isCardExpanded(lensId);
        // is this no tabs card expanded?

        log(logHeader + 'expanded=' + expanded);

        var expStyle = '';

        if (!expanded) {
            // if the current card is not expanded

            expStyle = ' style="display: none;" ';
        }

        lensHtml += '<div id="' + expElemId + '" ' + expStyle + '>' + expHtml + '</div>';
        // add the expanded contents inside a div to the lens HTML

        var collHtml = '';
        // the HTML for the collapsed contents

        if (true) {
            // add the collapsed content here

            var whenIntro = lens.WhenToUse;
            // default to the full when text

            const periodIndex = whenIntro.indexOf('.');
            // find the first period

            if (periodIndex >= 0) {
                // if we found one

                whenIntro = whenIntro.substr(0, periodIndex + 1);
                // get the first sentence of the when text
            }

            const eContentIdC = elemIdUtils.getElemId(lensId, elemIdUtils.CARD_TEXT_PREFIX, false);

            collHtml += '<div class="" id="' + eContentIdC + '">';

            collHtml += '<div class="section-caption">When to Use</div>';

            const eWhenIdC = elemIdUtils.getElemId(lensId, elemIdUtils.WHEN_CONTENT_PREFIX, false);

            collHtml += '<div class="section-content" id="' + eWhenIdC + '">' + whenIntro + '</div>';

            const spanMoreId = elemIdUtils.getElemId(lensId, elemIdUtils.SPAN_MORE_PREFIX, false);

            // the "more..." link which allows the user to expand the card

            // NOTE: s-more is the class for "more" links in cards without tabs

            collHtml += '<span id="' + spanMoreId + '" class="s-more"> ';
            collHtml += 'more...';
            collHtml += '</span>';

            collHtml += '</div>';
        }

        const collElemId = elemIdUtils.getElemId(lensId, elemIdUtils.CARD_COLLAPSED_CONTENT_PREFIX, undefined);

        var collStyle = '';

        if (expanded) {
            // if the current card is expanded

            collStyle = ' style="display: none;" ';
        }

        lensHtml += '<div id="' + collElemId + '" ' + collStyle + '>' + collHtml + '</div>';
        // add the collapsed contents inside a div to the lens HTML

        lensHtml += '</div>';  // .card-body

        lensHtml += '</div>';  // #card

        log(logHeader + ' leave');

        return lensHtml;
    },

    getHtmlForLensCardAsync: async function getHtmlForLensCardAsync(lens, showTabs, expanded, cardIndex, explicitWidthStr) {
        // showTabs: true => create the card with tabs
        //           false => create the cards with all info (when, how, etc.) visible, with headers and content in each section

        // expanded: true => create the card in expanded mode
        //           false => created the card in collapsed mode

        // cardIndex: zero-based index to the card on the web page

        // explicitWidthStr: undefined or empty string: do nothing
        //  otherwise, this string is a CSS width value for the lens card div
        //  for example:
        //      600px
        //      90%

        const logHdr = 'getHtmlForLensCardAsync: ';
        log(logHdr + 'enter');
        log(logHdr + 'lens name=' + lens.LensName);
        log(logHdr + 'showTabs=' + showTabs);
        log(logHdr + 'expanded=' + expanded);
        log(logHdr + 'cardIndex=' + cardIndex);
        log(logHdr + 'explicitWidthStr=' + explicitWidthStr);

        var lensHtml = '';

        if (showTabs) {
            // if we should show tabs

            lensHtml = await lensHtmlUtils._getHtmlForLensCardWithTabsAsync(lens, expanded, explicitWidthStr);
            // get HTML for the lens, with tabs
        }
        else {
            // if no tabs

            // NOTE: on iOS, if we add <video> elements to the page, Safari loads the entire video file
            // whether or not the user clicks the play button on the video thumbnail.
            // for this reason we only show video thumbnails for the first 3 videos on the page.
            // after that, we show a "Load Video" button that the user can click to load the video.
            // that way, we speed up the page and reduce bandwidth usage.

            var videoBtn = (cardIndex > 2);
            // cardIndex is zero-based
            // if this video is past the third one on the page, show a Load Video button
            // instead of a video thumbnail

            lensHtml = this._getHtmlForLensCardNoTabs(lens, videoBtn, explicitWidthStr);
            // get HTML for the lens, without tabs
        }

        log(logHdr + ' leave');

        return lensHtml;
    },

    getHtmlForLensesByAlphaAsync: async function getHtmlForLensesByAlphaAsync(lensDTOs) {
        // get HTML for the given array of lenses
        // display lenses alphabetically, not by group
        // NOTE: this function sorts the array of lenses

        const logHdr = 'getHtmlForLensesByAlphaAsync: ';
        log(logHdr + 'enter');

        lensDTOs.sort(function (a, b) { return (a.LensName > b.LensName) ? 1 : -1 });
        // sort lenses ascending by lens name

        var theHtml = '';

        var theCount = lensDTOs.length;

        log(logHdr + 'theCount=' + theCount);

        if (theCount < 1) {
            log(logHdr + 'no lenses so we are done');
            log(logHdr + 'leave');
            return '';
        }

        var showTabs = lensHtmlUtils.showTabsOrNot();
        // should we show tabs in the lens cards?
        // if the viewport width is below a specific value, we show lens cards without tabs
        // because the tabs don't fit, so they would wrap to a second row
        // which looks bad and could confuse the user as to which tab is selected

        log(logHdr + 'showTabs=' + showTabs);

        var lensesHtml = '';

        for (var zIndex = 0; zIndex < theCount; zIndex++) {
            // once for each card to display

            var lens = lensDTOs[zIndex];

            var lensId = lens.LensId;

            var expanded = tabsCardExpStorageUtils.isCardExpanded(lensId);
            // have we stored a request to expand the card?

            if (!expanded) {
                // if not expanded yet

                if (theCount === 1) {
                    // if there is precisely one lens in the array

                    tabsCardExpStorageUtils.addToLensIds(lensId);
                    // store the flag
                    // other code needs to know that this lens is expanded, eg code to show expand/collapse buttons

                    expanded = true;
                    // expand it, so the user doesn't have to click to see all the info in the card
                }
            }

            var widthStr = '';

            const cardWidth = cardWidthUtils.getCardWidth(lensId);
            // do we have a card width stored?
            // types of values
            //  0 => no width stored
            //  600 => numeric value greater than zero, number of pixels
            //  90% => string, percentage value

            log(logHdr + 'cardWidth=' + cardWidth);

            if (cardWidth === 0) {
                // if no value
            }
            else {
                if (typeof cardWidth === "number") {
                    widthStr = cardWidth + 'px';
                    // pixels
                }
                else {
                    widthStr = cardWidth;
                    // percentage string
                }
            }

            if (theCount === 1) {
                // if we will generate precisely one card

                widthStr = '90%';
                // make it wide
            }

            var oneLensHtml = await lensHtmlUtils.getHtmlForLensCardAsync(lens, showTabs, expanded, zIndex, widthStr);
            // get the HTML for one lens card

            lensesHtml += oneLensHtml;
        }

        theHtml = '<div id="divLensResults" class="d-flex flex-row flex-wrap justify-content-center">' + lensesHtml + '</div>';

        log(logHdr + 'leave');

        return theHtml;
    },

    getHtmlForLensesByGroupAsync: async function getHtmlForLensesByGroupAsync(lensDTOs) {
        // get HTML for the given array of lenses
        // display lenses by group
        // NOTE: this function sorts the array of lenses

        const logHdr = 'lensUIUtils.getHtmlForLensesByGroupAsync: ';
        log(logHdr + 'enter');

        var theHtml = '';
        const allLensCount = lensDTOs.length;

        log(logHdr + 'allLensCount=' + allLensCount);

        if (allLensCount < 1) {
            log(logHdr + 'no lenses so we are done');
            log(logHdr + 'leave');
            return theHtml;
        }

        var lensesInCats = lensUtils.getLensesInCategories(lensDTOs);
        // return an array of 5 arrays
        // each element in the first order array represents a category
        // the first order elements are in ascending category sort order
        // each first order element contains a (possibly empty) array of lenses
        // from the source list that belong to the category

        const catCount = lensesInCats.length;
        // get the number of categories

        log(logHdr + 'catCount=' + catCount);

        var categoriesHtml = '<hr id="hrAboveFirstCat" class="one-cat-hr" />';
        // start with an hr above the first category

        var totalIndex = 0;

        for (var catIndex = 0; catIndex < catCount; catIndex++) {
            // once for each category

            log(logHdr + 'Processing category ' + catIndex + ' of ' + catCount + '...');

            const lensesInCat = lensesInCats[catIndex];

            const lensesInCatCount = lensesInCat.length;

            log(logHdr + 'lensesInCatCount=' + lensesInCatCount);

            if (lensesInCatCount < 1)
                continue;
            // if no lenses in this category, skip it

            var firstLens = lensesInCat[0];
            var catName = firstLens.CategoryName;
            var catId = firstLens.CategoryId;

            log(logHdr + 'catName=' + catName);

            var showTabs = lensHtmlUtils.showTabsOrNot();
            // should we show tabs in the lens cards?
            // if the viewport width is below a specific value, we show lens cards without tabs
            // because the tabs don't fit, so they would wrap to a second row
            // which looks bad and could confuse the user as to which tab is selected

            log(logHdr + 'showTabs=' + showTabs);

            var lensesHtml = '';

            for (var lensIndex = 0; lensIndex < lensesInCatCount; lensIndex++) {
                // once for each lens in the category

                var lens = lensesInCat[lensIndex];

                log(logHdr + 'lens name=' + lens.LensName);

                var lensId = lens.LensId;

                log(logHdr + 'lensId=' + lensId);

                var expanded = tabsCardExpStorageUtils.isCardExpanded(lensId);
                // have we stored a request to expand the card?

                var widthStr = '';

                const cardWidth = cardWidthUtils.getCardWidth(lensId);
                // do we have a card width stored?
                // types of values
                //  0 => no width stored
                //  600 => numeric value greater than zero, number of pixels
                //  90% => string, percentage value

                log(logHdr + 'cardWidth=' + cardWidth);

                if (cardWidth === 0) {
                    // if no value
                }
                else {
                    if (typeof cardWidth === "number") {
                        widthStr = cardWidth + 'px';
                        // pixels
                    }
                    else {
                        widthStr = cardWidth;
                        // percentage string
                    }
                }

                var oneLensHtml = await lensHtmlUtils.getHtmlForLensCardAsync(lens, showTabs, expanded, totalIndex, widthStr);
                // get the HTML for one lens card
                // showTabs: should we show tabs or flow sections?
                // expanded: is the card expanded or collapsed?

                totalIndex += 1;
                // increment index of the card on the page

                lensesHtml += oneLensHtml;
            }

            var oneCatHtml = '<h5 id="catName' + catId + '" class="category-name">' + catName + '</h5>';
            // start with an element for the category name

            oneCatHtml += '<div id="divCategory' + catId + '" class="div-category-lenses d-flex flex-row flex-wrap justify-content-left">';
            // append a parent div for the lenses in the category

            oneCatHtml += lensesHtml;
            // append the lenses

            oneCatHtml += '</div>';
            // close the parent div

            categoriesHtml += oneCatHtml;
            // append the HTML for this category to the categories HTML

            categoriesHtml += '<hr id="hrCategory' + catId + '" class="one-cat-hr" />';
            // append a horizontal rule
        }

        theHtml = '<div id="divResultsByCat">' + categoriesHtml + '</div>';
        // wrap the categories HTML in a parent div so we can style it

        log(logHdr + 'leave');
        return theHtml;
    }
};